Initial Commit

This commit is contained in:
Aodhan Collins
2025-07-27 01:17:19 +01:00
commit 6319df7d29
9 changed files with 1855 additions and 0 deletions

19
.env.example Normal file
View File

@@ -0,0 +1,19 @@
# Docker Dashboard Configuration
# Copy this file to .env and customize the values
# Server Configuration
PORT=5000
HOST=0.0.0.0
DEBUG=True
# Portainer Integration (Optional)
PORTAINER_URL=https://port.liveaodh.com
PORTAINER_USERNAME=aodhan
PORTAINER_PASSWORD=8yFdXkx274aSRX
# Docker Configuration
DOCKER_HOST=unix:///var/run/docker.sock
# Application Settings
REFRESH_INTERVAL=30
MAX_CONTAINERS=50

121
.gitignore vendored Normal file
View File

@@ -0,0 +1,121 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual Environment
venv/
env/
ENV/
env.bak/
venv.bak/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Database
*.db
*.sqlite
*.sqlite3
# Logs
*.log
logs/
# Environment Variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Runtime files
*.pid
*.sock
# Docker
.dockerignore
# Node.js (if any)
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Project specific
dashboard.db
dashboard.pid
stop.sh
logs/

234
README.md Normal file
View File

@@ -0,0 +1,234 @@
# Docker Dashboard
A modern, responsive web dashboard for managing and monitoring Docker containers with SQLite database integration and manual customization capabilities.
## Features
- **Dynamic Container Tiles**: Automatically discovers and displays all Docker containers
- **SQLite Database**: Persistent storage for custom tile configurations
- **Manual Customization**: Edit display names, icons, colors, descriptions, and URLs
- **Real-time Monitoring**: Live status updates and system metrics
- **Responsive Design**: Works on desktop, tablet, and mobile devices
- **Portainer Integration**: Optional enhanced functionality with Portainer API
- **Container Management**: Start, stop, and restart containers directly from the dashboard
## Quick Start
### Prerequisites
- Docker installed and running
- Python 3.7+
- Docker socket access (typically requires sudo)
### Installation
1. **Clone or download the project**
2. **Install dependencies**:
```bash
pip3 install -r requirements.txt
```
3. **Configure environment**:
```bash
cp .env.example .env
# Edit .env with your specific settings
```
4. **Run the setup script** (recommended):
```bash
chmod +x setup.sh
./setup.sh
```
5. **Or run manually**:
```bash
sudo python3 api.py
```
6. **Access the dashboard**:
Open your browser to `http://localhost:5000`
### Manual Setup
If you prefer to set up manually:
1. **Install Python dependencies**:
```bash
pip3 install flask flask-cors docker requests python-dotenv
```
2. **Initialize database**:
```bash
python3 -c "import database; db = database.DatabaseManager(); db.init_db()"
```
3. **Start the application**:
```bash
sudo python3 api.py
```
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | Server port | 5000 |
| `HOST` | Server host | 0.0.0.0 |
| `DEBUG` | Debug mode | False |
| `REFRESH_INTERVAL` | Auto-refresh interval (seconds) | 30 |
| `PORTAINER_URL` | Portainer URL (optional) | - |
| `PORTAINER_USERNAME` | Portainer username | - |
| `PORTAINER_PASSWORD` | Portainer password | - |
### Portainer Integration (Optional)
To use Portainer for enhanced container management:
1. Set up Portainer on your Docker host
2. Configure the environment variables:
```bash
PORTAINER_URL=https://your-portainer-instance.com
PORTAINER_USERNAME=your-username
PORTAINER_PASSWORD=your-password
```
## Usage
### Dashboard Overview
- **Server Information**: CPU, memory, and disk usage
- **Container Tiles**: Visual representation of all Docker containers
- **Live Status**: Real-time container status (running/stopped)
- **Uptime Display**: Container uptime information
- **Quick Actions**: Open, edit, or manage containers
### Customizing Tiles
1. **Click the "Edit" button** on any container tile
2. **Modify the fields**:
- **Display Name**: Custom name shown on the tile
- **Icon**: Font Awesome icon class (e.g., `fas fa-cube`)
- **Color**: Tailwind CSS color (e.g., `blue`, `green`, `red`)
- **Description**: Additional description text
- **URL**: Custom URL for the application
3. **Save changes** - updates are immediately reflected
### Syncing Containers
- **Automatic**: Containers sync every 30 seconds
- **Manual**: Click "Sync Apps" button to force refresh
- **New containers**: Automatically appear as new tiles
- **Removed containers**: Tiles are removed from display
## API Endpoints
### System Information
- `GET /api/system` - System metrics and uptime
### Containers
- `GET /api/containers` - List all Docker containers
- `GET /api/health/<container_name>` - Check container health
### Applications (Database)
- `GET /api/applications` - Get all applications from database
- `GET /api/applications/<container_id>` - Get specific application
- `PUT /api/applications/<container_id>` - Update application data
- `DELETE /api/applications/<container_id>` - Delete application
- `POST /api/applications/sync` - Sync with current containers
### Portainer (Optional)
- `GET /api/portainer/containers` - Get containers via Portainer
- `POST /api/portainer/container/<action>/<container_id>` - Manage containers
## Database Schema
The SQLite database (`dashboard.db`) contains the following table:
```sql
CREATE TABLE applications (
container_id TEXT PRIMARY KEY,
container_name TEXT NOT NULL,
display_name TEXT,
image TEXT,
status TEXT,
ports TEXT,
uptime TEXT,
icon TEXT DEFAULT 'fas fa-cube',
color TEXT DEFAULT 'gray',
description TEXT,
url TEXT,
custom_data TEXT
);
```
## File Structure
```
├── api.py # Main Flask API server
├── database.py # SQLite database manager
├── template.html # Frontend dashboard
├── portainer_integration.py # Portainer API integration
├── setup.sh # Setup and launch script
├── stop.sh # Stop script (generated)
├── .env # Environment variables (create from .env.example)
├── .env.example # Environment template
├── requirements.txt # Python dependencies
├── .gitignore # Git ignore rules
├── dashboard.db # SQLite database (created automatically)
└── logs/
└── dashboard.log # Application logs
```
## Troubleshooting
### Docker Permission Issues
If you see "Permission denied" errors:
```bash
# Add your user to docker group
sudo usermod -aG docker $USER
# Log out and back in
```
### Port Issues
If port 5000 is already in use:
```bash
# Edit .env file and change PORT
PORT=5001
```
### Database Issues
To reset the database:
```bash
rm dashboard.db
python3 -c "import database; db = database.DatabaseManager(); db.init_db()"
```
### View Logs
```bash
tail -f logs/dashboard.log
```
## Development
### Adding New Features
1. **Backend**: Modify `api.py` for new endpoints
2. **Database**: Update `database.py` for schema changes
3. **Frontend**: Edit `template.html` for UI changes
### Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## License
This project is open source and available under the [MIT License](LICENSE).
## Support
For issues or questions:
1. Check the troubleshooting section above
2. Review the logs in `logs/dashboard.log`
3. Open an issue on the project repository

392
api.py Normal file
View File

@@ -0,0 +1,392 @@
#!/usr/bin/env python3
"""
Docker Dashboard API
Fetches container data from Docker instances and provides endpoints for the frontend
"""
import json
import subprocess
import time
import logging
from datetime import datetime
from flask import Flask, jsonify, render_template, request, send_from_directory
from flask_cors import CORS
import docker
import psutil
import requests
import os
from dotenv import load_dotenv
from portainer_integration import PortainerClient
from database import db
# Load environment variables
load_dotenv()
app = Flask(__name__)
CORS(app)
class DockerService:
def __init__(self):
try:
self.client = docker.from_env()
print("Docker client initialized successfully")
except Exception as e:
logging.error(f"Failed to initialize Docker client: {e}")
self.client = None
def get_containers(self):
"""Get all containers with their status"""
if not self.client:
return []
containers = []
try:
for container in self.client.containers.list(all=True):
container_data = {
'id': container.id[:12],
'name': container.name,
'status': container.status,
'image': container.image.tags[0] if container.image.tags else container.image.short_id,
'ports': self._get_container_ports(container),
'created': container.attrs['Created'],
'uptime': self._calculate_uptime(container),
'labels': container.labels,
'environment': container.attrs['Config'].get('Env', []),
'mounts': self._get_mounts(container),
'networks': self._get_networks(container)
}
containers.append(container_data)
except Exception as e:
logging.error(f"Error fetching containers: {e}")
return []
return containers
def _get_container_ports(self, container):
"""Extract port mappings from container"""
ports = []
try:
port_bindings = container.attrs['NetworkSettings']['Ports']
for container_port, host_configs in port_bindings.items():
if host_configs:
for host_config in host_configs:
ports.append({
'container_port': container_port,
'host_ip': host_config.get('HostIp', '0.0.0.0'),
'host_port': host_config.get('HostPort')
})
except Exception:
pass
return ports
def _calculate_uptime(self, container):
"""Calculate container uptime"""
if container.status != 'running':
return "Down"
try:
started_at = container.attrs['State']['StartedAt']
started_time = datetime.fromisoformat(started_at.replace('Z', '+00:00'))
uptime = datetime.now(started_time.tzinfo) - started_time
days = uptime.days
hours, remainder = divmod(uptime.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
if days > 0:
return f"{days}d {hours}h"
elif hours > 0:
return f"{hours}h {minutes}m"
else:
return f"{minutes}m"
except Exception:
return "Unknown"
def _get_mounts(self, container):
"""Get container volume mounts"""
mounts = []
try:
for mount in container.attrs['Mounts']:
mounts.append({
'type': mount.get('Type'),
'source': mount.get('Source'),
'destination': mount.get('Destination'),
'mode': mount.get('Mode')
})
except Exception:
pass
return mounts
def _get_networks(self, container):
"""Get container network information"""
networks = []
try:
networks_config = container.attrs['NetworkSettings']['Networks']
for network_name, network_config in networks_config.items():
networks.append({
'name': network_name,
'ip_address': network_config.get('IPAddress'),
'gateway': network_config.get('Gateway'),
'mac_address': network_config.get('MacAddress')
})
except Exception:
pass
return networks
def get_system_info(self):
"""Get system information"""
try:
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')
return {
'cpu_percent': psutil.cpu_percent(),
'memory': {
'total': memory.total,
'available': memory.available,
'percent': memory.percent,
'used': memory.used
},
'disk': {
'total': disk.total,
'used': disk.used,
'free': disk.free,
'percent': (disk.used / disk.total) * 100
},
'uptime': self._get_system_uptime()
}
except Exception as e:
logging.error(f"Error getting system info: {e}")
return {}
def _get_system_uptime(self):
"""Get system uptime"""
try:
with open('/proc/uptime', 'r') as f:
uptime_seconds = float(f.readline().split()[0])
days = int(uptime_seconds // 86400)
hours = int((uptime_seconds % 86400) // 3600)
minutes = int((uptime_seconds % 3600) // 60)
return f"{days}d {hours}h {minutes}m"
except Exception:
return "Unknown"
def check_service_health(self, url, port=None):
"""Check if a service is responding"""
try:
if port:
url = f"http://{url}:{port}"
response = requests.get(url, timeout=5)
return response.status_code == 200
except Exception:
return False
# Configuration from environment variables
PORT = int(os.getenv('PORT', 5000))
HOST = os.getenv('HOST', '0.0.0.0')
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
REFRESH_INTERVAL = int(os.getenv('REFRESH_INTERVAL', 30))
# Initialize Docker service
docker_service = DockerService()
# Initialize Portainer client
portainer_client = None
portainer_url = os.getenv('PORTAINER_URL', 'http://localhost:9000')
portainer_username = os.getenv('PORTAINER_USERNAME')
portainer_password = os.getenv('PORTAINER_PASSWORD')
if portainer_username and portainer_password:
portainer_client = PortainerClient(portainer_url, portainer_username, portainer_password)
if portainer_client.authenticate():
logging.info("Portainer client authenticated successfully")
else:
logging.error("Portainer authentication failed - using standard Docker API")
portainer_client = None
@app.route('/')
def dashboard():
"""Serve the dashboard"""
return render_template('template.html')
@app.route('/api/containers')
def get_containers():
"""Get all containers"""
containers = docker_service.get_containers()
return jsonify(containers)
@app.route('/api/system')
def get_system_info():
"""Get system information"""
system_info = docker_service.get_system_info()
return jsonify(system_info)
@app.route('/api/health/<container_name>')
def check_container_health(container_name):
"""Check health of a specific container"""
try:
# Get application from database for custom health check URL
app = db.get_application(container_name)
if app and app.get('url') != '#':
url = app['url']
else:
url = f'http://{container_name}.local'
try:
response = requests.get(url, timeout=5)
return jsonify({
'healthy': response.status_code < 400,
'status_code': response.status_code,
'url': url
})
except requests.RequestException:
return jsonify({
'healthy': False,
'error': 'Service not responding',
'url': url
})
except Exception as e:
return jsonify({
'healthy': False,
'error': str(e)
}), 500
@app.route('/api/portainer/containers')
def get_portainer_containers():
"""Get containers from Portainer if available"""
try:
# This would need Portainer API configuration
# For now, return standard Docker containers
return get_containers()
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/portainer/containers')
def get_portainer_containers_list():
"""Get containers from Portainer"""
if not portainer_client:
return jsonify({'error': 'Portainer not configured'}), 503
try:
containers = portainer_client.get_containers()
return jsonify(containers)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/portainer/endpoints')
def get_portainer_endpoints():
"""Get Portainer endpoints"""
if not portainer_client:
return jsonify({'error': 'Portainer not configured'}), 503
try:
endpoints = portainer_client.get_endpoints()
return jsonify(endpoints)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/portainer/stacks')
def get_portainer_stacks():
"""Get Docker stacks from Portainer"""
if not portainer_client:
return jsonify({'error': 'Portainer not configured'}), 503
try:
stacks = portainer_client.get_stacks()
return jsonify(stacks)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/portainer/container/<action>/<container_id>', methods=['POST'])
def manage_portainer_container(action, container_id):
"""Manage containers via Portainer"""
if not portainer_client:
return jsonify({'error': 'Portainer not configured'}), 503
if action not in ['start', 'stop', 'restart']:
return jsonify({'error': 'Invalid action'}), 400
try:
success = False
if action == 'start':
success = portainer_client.start_container(container_id)
elif action == 'stop':
success = portainer_client.stop_container(container_id)
elif action == 'restart':
success = portainer_client.restart_container(container_id)
return jsonify({'success': success})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/applications')
def get_applications():
"""Get all applications from database"""
try:
applications = db.get_applications()
# Sync with current containers
containers = docker_service.get_containers()
container_map = {c['id']: c for c in containers}
# Update applications with live container data
for app in applications:
container = container_map.get(app['container_id'])
if container:
app.update({
'status': container.get('status', 'unknown'),
'uptime': container.get('uptime', 'unknown'),
'image': container.get('image', ''),
'ports': container.get('ports', [])
})
return jsonify({'applications': applications})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/applications/<container_id>', methods=['GET', 'PUT', 'DELETE'])
def manage_application(container_id):
"""Manage individual application"""
try:
if request.method == 'GET':
app = db.get_application(container_id)
if app:
return jsonify(app)
return jsonify({'error': 'Application not found'}), 404
elif request.method == 'PUT':
data = request.json
db.update_application(container_id, data)
return jsonify({'success': True, 'message': 'Application updated'})
elif request.method == 'DELETE':
db.delete_application(container_id)
return jsonify({'success': True, 'message': 'Application deleted'})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/applications/sync', methods=['POST'])
def sync_applications():
"""Sync applications with current containers"""
try:
containers = docker_service.get_containers()
# Sync each container with database
for container in containers:
db.upsert_application({
'id': container['id'],
'name': container['name'],
'description': container['image'],
'url': f"http://localhost:{container['ports'][0]['host_port']}" if container['ports'] else '#'
})
return jsonify({'success': True, 'message': 'Applications synced successfully'})
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(host=HOST, port=PORT, debug=DEBUG)

194
database.py Normal file
View File

@@ -0,0 +1,194 @@
import sqlite3
import os
import json
from datetime import datetime
class DatabaseManager:
def __init__(self, db_path='dashboard.db'):
self.db_path = db_path
self.init_database()
def init_database(self):
"""Initialize the database with required tables"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# Create applications table for storing custom app data
cursor.execute('''
CREATE TABLE IF NOT EXISTS applications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
container_id TEXT UNIQUE,
container_name TEXT NOT NULL,
display_name TEXT,
icon TEXT DEFAULT 'fas fa-cube',
color TEXT DEFAULT 'gray',
description TEXT,
url TEXT,
custom_data TEXT, -- JSON for additional custom fields
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Create settings table for general configuration
cursor.execute('''
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
def get_connection(self):
"""Get database connection"""
return sqlite3.connect(self.db_path)
def upsert_application(self, container_data):
"""Insert or update application data"""
conn = self.get_connection()
cursor = conn.cursor()
container_id = container_data.get('id')
container_name = container_data.get('name')
display_name = container_data.get('display_name', container_name)
icon = container_data.get('icon', 'fas fa-cube')
color = container_data.get('color', 'gray')
description = container_data.get('description', container_data.get('image', ''))
url = container_data.get('url', '#')
custom_data = json.dumps(container_data.get('custom_data', {}))
cursor.execute('''
INSERT OR REPLACE INTO applications
(container_id, container_name, display_name, icon, color, description, url, custom_data, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
''', (container_id, container_name, display_name, icon, color, description, url, custom_data))
conn.commit()
conn.close()
def get_applications(self):
"""Get all applications"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT container_id, container_name, display_name, icon, color, description, url, custom_data
FROM applications
ORDER BY display_name
''')
results = cursor.fetchall()
conn.close()
applications = []
for row in results:
app = {
'container_id': row[0],
'container_name': row[1],
'display_name': row[2],
'icon': row[3],
'color': row[4],
'description': row[5],
'url': row[6],
'custom_data': json.loads(row[7]) if row[7] else {}
}
applications.append(app)
return applications
def get_application(self, container_id):
"""Get specific application by container_id"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT container_id, container_name, display_name, icon, color, description, url, custom_data
FROM applications
WHERE container_id = ?
''', (container_id,))
result = cursor.fetchone()
conn.close()
if result:
return {
'container_id': result[0],
'container_name': result[1],
'display_name': result[2],
'icon': result[3],
'color': result[4],
'description': result[5],
'url': result[6],
'custom_data': json.loads(result[7]) if result[7] else {}
}
return None
def update_application(self, container_id, updates):
"""Update application data"""
conn = self.get_connection()
cursor = conn.cursor()
set_clause = []
values = []
for key, value in updates.items():
if key == 'custom_data':
value = json.dumps(value)
set_clause.append(f"{key} = ?")
values.append(value)
if set_clause:
set_clause.append("updated_at = CURRENT_TIMESTAMP")
values.append(container_id)
query = f"UPDATE applications SET {', '.join(set_clause)} WHERE container_id = ?"
cursor.execute(query, values)
conn.commit()
conn.close()
def delete_application(self, container_id):
"""Delete application"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM applications WHERE container_id = ?', (container_id,))
conn.commit()
conn.close()
def sync_with_containers(self, containers):
"""Sync applications with current containers"""
conn = self.get_connection()
cursor = conn.cursor()
# Get existing container IDs
cursor.execute('SELECT container_id FROM applications')
existing_ids = {row[0] for row in cursor.fetchall()}
# Insert new containers
for container in containers:
container_id = container.get('id')
if container_id not in existing_ids:
self.upsert_application({
'id': container_id,
'name': container.get('name', ''),
'description': container.get('image', ''),
'url': self._generate_url_from_ports(container.get('ports', []))
})
conn.close()
def _generate_url_from_ports(self, ports):
"""Generate URL from container ports"""
if ports and len(ports) > 0:
port = ports[0]
return f"http://localhost:{port.get('host_port', port.get('private_port', 80))}"
return '#'
# Global database instance
db = DatabaseManager()

255
portainer_integration.py Normal file
View File

@@ -0,0 +1,255 @@
#!/usr/bin/env python3
"""
Portainer Integration Module
Provides enhanced Docker management through Portainer API
"""
import requests
import json
from typing import Dict, List, Optional
import os
class PortainerClient:
def __init__(self, base_url: str, username: str = None, password: str = None):
self.base_url = base_url.rstrip('/')
self.username = username or os.getenv('PORTAINER_USERNAME')
self.password = password or os.getenv('PORTAINER_PASSWORD')
self.token = None
self.endpoint_id = None
def authenticate(self) -> bool:
"""Authenticate with Portainer API"""
try:
auth_url = f"{self.base_url}/api/auth"
payload = {
"username": self.username,
"password": self.password
}
response = requests.post(auth_url, json=payload)
if response.status_code == 200:
self.token = response.json().get('jwt')
return True
return False
except Exception as e:
print(f"Portainer authentication failed: {e}")
return False
def get_endpoints(self) -> List[Dict]:
"""Get available endpoints"""
if not self.token:
return []
try:
headers = {'Authorization': f'Bearer {self.token}'}
response = requests.get(f"{self.base_url}/api/endpoints", headers=headers)
if response.status_code == 200:
return response.json()
return []
except Exception as e:
print(f"Error fetching endpoints: {e}")
return []
def get_containers(self, endpoint_id: int = None) -> List[Dict]:
"""Get containers from Portainer"""
if not self.token:
return []
try:
if not endpoint_id:
endpoints = self.get_endpoints()
if endpoints:
endpoint_id = endpoints[0]['Id']
else:
return []
headers = {'Authorization': f'Bearer {self.token}'}
url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/containers/json?all=true"
response = requests.get(url, headers=headers)
if response.status_code == 200:
containers = response.json()
return self._format_portainer_containers(containers)
return []
except Exception as e:
print(f"Error fetching containers from Portainer: {e}")
return []
def _format_portainer_containers(self, containers: List[Dict]) -> List[Dict]:
"""Format Portainer containers to match our standard format"""
formatted = []
for container in containers:
formatted_container = {
'id': container.get('Id', '')[:12],
'name': container.get('Names', [''])[0].lstrip('/'),
'status': container.get('State', 'unknown'),
'image': container.get('Image', ''),
'ports': self._format_ports(container.get('Ports', [])),
'created': container.get('Created', ''),
'uptime': self._calculate_uptime(container),
'labels': container.get('Labels', {}),
'environment': [],
'mounts': self._format_mounts(container.get('Mounts', [])),
'networks': self._format_networks(container.get('NetworkSettings', {}).get('Networks', {}))
}
formatted.append(formatted_container)
return formatted
def _format_ports(self, ports: List[Dict]) -> List[Dict]:
"""Format port mappings"""
formatted_ports = []
for port in ports:
formatted_ports.append({
'container_port': f"{port.get('PrivatePort', '')}/{port.get('Type', 'tcp')}",
'host_ip': port.get('IP', '0.0.0.0'),
'host_port': str(port.get('PublicPort', ''))
})
return formatted_ports
def _format_mounts(self, mounts: List[Dict]) -> List[Dict]:
"""Format volume mounts"""
formatted_mounts = []
for mount in mounts:
formatted_mounts.append({
'type': mount.get('Type'),
'source': mount.get('Source'),
'destination': mount.get('Destination'),
'mode': mount.get('Mode')
})
return formatted_mounts
def _format_networks(self, networks: Dict) -> List[Dict]:
"""Format network information"""
formatted_networks = []
for network_name, network_config in networks.items():
formatted_networks.append({
'name': network_name,
'ip_address': network_config.get('IPAddress'),
'gateway': network_config.get('Gateway'),
'mac_address': network_config.get('MacAddress')
})
return formatted_networks
def _calculate_uptime(self, container: Dict) -> str:
"""Calculate uptime from container info"""
status = container.get('State')
if status != 'running':
return 'Down'
# For now, return simplified uptime
# In a real implementation, you'd parse the Created field
return 'Running'
def get_stacks(self, endpoint_id: int = None) -> List[Dict]:
"""Get Docker stacks from Portainer"""
if not self.token:
return []
try:
if not endpoint_id:
endpoints = self.get_endpoints()
if endpoints:
endpoint_id = endpoints[0]['Id']
else:
return []
headers = {'Authorization': f'Bearer {self.token}'}
url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/stacks"
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
return []
except Exception as e:
print(f"Error fetching stacks from Portainer: {e}")
return []
def get_volumes(self, endpoint_id: int = None) -> List[Dict]:
"""Get Docker volumes from Portainer"""
if not self.token:
return []
try:
if not endpoint_id:
endpoints = self.get_endpoints()
if endpoints:
endpoint_id = endpoints[0]['Id']
else:
return []
headers = {'Authorization': f'Bearer {self.token}'}
url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/volumes"
response = requests.get(url, headers=headers)
if response.status_code == 200:
volumes_data = response.json()
return volumes_data.get('Volumes', [])
return []
except Exception as e:
print(f"Error fetching volumes from Portainer: {e}")
return []
def get_networks(self, endpoint_id: int = None) -> List[Dict]:
"""Get Docker networks from Portainer"""
if not self.token:
return []
try:
if not endpoint_id:
endpoints = self.get_endpoints()
if endpoints:
endpoint_id = endpoints[0]['Id']
else:
return []
headers = {'Authorization': f'Bearer {self.token}'}
url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/networks"
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
return []
except Exception as e:
print(f"Error fetching networks from Portainer: {e}")
return []
def start_container(self, container_id: str, endpoint_id: int = None) -> bool:
"""Start a container"""
return self._container_action(container_id, 'start', endpoint_id)
def stop_container(self, container_id: str, endpoint_id: int = None) -> bool:
"""Stop a container"""
return self._container_action(container_id, 'stop', endpoint_id)
def restart_container(self, container_id: str, endpoint_id: int = None) -> bool:
"""Restart a container"""
return self._container_action(container_id, 'restart', endpoint_id)
def _container_action(self, container_id: str, action: str, endpoint_id: int = None) -> bool:
"""Perform container action"""
if not self.token:
return False
try:
if not endpoint_id:
endpoints = self.get_endpoints()
if endpoints:
endpoint_id = endpoints[0]['Id']
else:
return False
headers = {'Authorization': f'Bearer {self.token}'}
url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/containers/{container_id}/{action}"
response = requests.post(url, headers=headers)
return response.status_code == 204
except Exception as e:
print(f"Error performing {action} on container: {e}")
return False

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
flask==2.3.3
flask-cors==4.0.0
docker==6.1.3
psutil==5.9.5
requests==2.31.0
python-dotenv==1.0.0

255
setup.sh Executable file
View File

@@ -0,0 +1,255 @@
#!/bin/bash
# Docker Dashboard Setup Script
# This script sets up and launches the Docker Dashboard as a background process
set -e
echo "🚀 Docker Dashboard Setup Script"
echo "================================"
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if running as root for Docker access
check_permissions() {
if [[ $EUID -ne 0 ]]; then
print_warning "This script needs to run with sudo for Docker access"
print_warning "Rerunning with sudo..."
exec sudo "$0" "$@"
fi
}
# Check if Docker is running
check_docker() {
if ! systemctl is-active --quiet docker; then
print_warning "Docker service is not running. Starting Docker..."
systemctl start docker
fi
if ! docker info >/dev/null 2>&1; then
print_error "Docker is not accessible. Please check Docker installation"
exit 1
fi
print_status "Docker is running and accessible"
}
# Install Python dependencies
install_dependencies() {
print_status "Installing Python dependencies..."
# Check if pip is available
if ! command -v pip3 &> /dev/null; then
print_error "pip3 is not installed. Please install pip3 first"
exit 1
fi
# Install requirements
if [[ -f "requirements.txt" ]]; then
pip3 install -r requirements.txt
else
pip3 install flask flask-cors docker requests python-dotenv
fi
print_status "Dependencies installed successfully"
}
# Setup environment file
setup_environment() {
if [[ ! -f ".env" ]]; then
print_status "Creating .env file from template..."
cp .env.example .env
print_status "Please edit .env file with your specific configuration"
else
print_status ".env file already exists"
fi
# Source the environment file
source .env
# Set defaults if not provided
export PORT=${PORT:-5000}
export HOST=${HOST:-0.0.0.0}
export DEBUG=${DEBUG:-False}
export REFRESH_INTERVAL=${REFRESH_INTERVAL:-30}
print_status "Environment configured"
}
# Initialize database
initialize_database() {
print_status "Initializing database..."
# Create database if it doesn't exist
if [[ ! -f "dashboard.db" ]]; then
python3 -c "
import database
db = database.DatabaseManager()
db.init_db()
print('Database initialized successfully')
"
else
print_status "Database already exists"
fi
}
# Kill existing processes
kill_existing_processes() {
print_status "Checking for existing processes..."
# Kill existing Python processes on the port
PID=$(lsof -t -i :${PORT} 2>/dev/null || echo "")
if [[ -n "$PID" ]]; then
print_warning "Killing existing process on port ${PORT} (PID: $PID)"
kill -TERM "$PID" 2>/dev/null || true
sleep 2
kill -KILL "$PID" 2>/dev/null || true
fi
}
# Launch application
launch_application() {
print_status "Launching Docker Dashboard..."
# Create logs directory
mkdir -p logs
# Launch the application in background
nohup python3 api.py > logs/dashboard.log 2>&1 &
# Get the process ID
APP_PID=$!
# Wait a moment for the app to start
sleep 3
# Check if the process is still running
if kill -0 "$APP_PID" 2>/dev/null; then
print_status "Application started successfully (PID: $APP_PID)"
print_status "Dashboard available at: http://localhost:${PORT}"
print_status "Logs available at: logs/dashboard.log"
# Save PID to file
echo "$APP_PID" > dashboard.pid
print_status ""
print_status "To stop the application, run:"
print_status " sudo ./stop.sh"
print_status ""
print_status "To view logs:"
print_status " tail -f logs/dashboard.log"
else
print_error "Failed to start application. Check logs/dashboard.log for details"
exit 1
fi
}
# Create stop script
create_stop_script() {
cat > stop.sh << 'EOF'
#!/bin/bash
# Stop Docker Dashboard Script
if [[ -f "dashboard.pid" ]]; then
PID=$(cat dashboard.pid)
if kill -0 "$PID" 2>/dev/null; then
echo "Stopping Docker Dashboard (PID: $PID)..."
kill -TERM "$PID"
sleep 2
if kill -0 "$PID" 2>/dev/null; then
echo "Force stopping..."
kill -KILL "$PID"
fi
rm dashboard.pid
echo "Application stopped successfully"
else
echo "Application is not running"
rm -f dashboard.pid
fi
else
echo "PID file not found. Attempting to kill by port..."
PID=$(lsof -t -i :5000 2>/dev/null || echo "")
if [[ -n "$PID" ]]; then
echo "Stopping process on port 5000 (PID: $PID)..."
kill -TERM "$PID"
sleep 2
if kill -0 "$PID" 2>/dev/null; then
echo "Force stopping..."
kill -KILL "$PID"
fi
echo "Application stopped successfully"
else
echo "No application found running on port 5000"
fi
fi
EOF
chmod +x stop.sh
print_status "Stop script created: stop.sh"
}
# Main execution
main() {
print_status "Starting Docker Dashboard setup..."
check_permissions
check_docker
install_dependencies
setup_environment
initialize_database
kill_existing_processes
create_stop_script
launch_application
print_status ""
print_status "🎉 Docker Dashboard setup complete!"
print_status "Dashboard is running at: http://localhost:${PORT}"
print_status ""
print_status "Next steps:"
print_status "1. Open your browser to http://localhost:${PORT}"
print_status "2. Click 'Sync Apps' to load your Docker containers"
print_status "3. Edit tiles by clicking the 'Edit' button on each tile"
print_status "4. Use ./stop.sh to stop the application"
}
# Handle command line arguments
case "${1:-}" in
"--help"|"-h")
echo "Usage: $0 [--help]"
echo ""
echo "This script sets up and launches the Docker Dashboard"
echo ""
echo "Options:"
echo " --help, -h Show this help message"
exit 0
;;
*)
main
;;
esac

379
template.html Normal file
View File

@@ -0,0 +1,379 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home Server Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.card-hover {
transition: all 0.3s ease;
}
.card-hover:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.status-indicator {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
</style>
</head>
<body class="bg-gray-900 min-h-screen text-gray-100">
<div class="container mx-auto px-4 py-8">
<!-- Header -->
<header class="mb-8">
<h1 class="text-4xl font-bold text-white mb-2">Home Server Dashboard</h1>
<p class="text-gray-400">Centralized access to your self-hosted applications</p>
<div class="flex items-center space-x-4 mt-4">
<button onclick="syncApplications()" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg text-sm">
<i class="fas fa-sync-alt mr-2"></i>Sync Apps
</button>
<button onclick="refreshData()" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg text-sm">
<i class="fas fa-refresh mr-2"></i>Refresh
</button>
</div>
</header>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Server Info Card -->
<div class="bg-gray-800 rounded-xl shadow-md overflow-hidden col-span-1 border border-gray-700">
<div class="bg-indigo-600 p-4">
<h2 class="text-xl font-semibold text-white">Server Information</h2>
</div>
<div class="p-6 space-y-4">
<div>
<div class="flex justify-between items-center mb-2">
<span class="text-gray-300">CPU Usage</span>
<span id="cpu-usage" class="text-white font-semibold">--</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2">
<div id="cpu-bar" class="bg-indigo-500 h-2 rounded-full" style="width: 0%"></div>
</div>
</div>
<div>
<div class="flex justify-between items-center mb-2">
<span class="text-gray-300">Memory Usage</span>
<span id="memory-usage" class="text-white font-semibold">--</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2">
<div id="memory-bar" class="bg-green-500 h-2 rounded-full" style="width: 0%"></div>
</div>
</div>
<div>
<div class="flex justify-between items-center mb-2">
<span class="text-gray-300">Disk Usage</span>
<span id="disk-usage" class="text-white font-semibold">--</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2">
<div id="disk-bar" class="bg-yellow-500 h-2 rounded-full" style="width: 0%"></div>
</div>
</div>
<div class="pt-4 border-t border-gray-700">
<div class="flex justify-between">
<span class="text-gray-300">Uptime</span>
<span id="server-uptime" class="text-white">--</span>
</div>
</div>
</div>
</div>
<!-- Applications Grid -->
<div class="bg-gray-800 rounded-xl shadow-md overflow-hidden col-span-1 lg:col-span-2 border border-gray-700">
<div class="bg-indigo-600 p-4">
<div class="flex justify-between items-center">
<h2 class="text-xl font-semibold text-white">Hosted Applications</h2>
<div class="flex items-center">
<div id="refresh-indicator" class="hidden">
<i class="fas fa-spinner fa-spin text-white mr-2"></i>
</div>
<span id="app-count" class="text-sm text-gray-300">0 apps</span>
</div>
</div>
</div>
<div class="p-6">
<div id="container-grid" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="text-center py-8">
<i class="fas fa-spinner fa-spin text-2xl text-gray-400 mb-2"></i>
<p class="text-gray-400">Loading applications...</p>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="mt-8 bg-gray-800 rounded-xl shadow-md overflow-hidden border border-gray-700">
<div class="bg-indigo-600 p-4">
<h2 class="text-xl font-semibold text-white">Recent Activity</h2>
</div>
<div class="p-6 space-y-4">
<div class="flex items-start">
<div class="flex-shrink-0 h-10 w-10 rounded-full bg-green-100 flex items-center justify-center mr-4">
<i class="fas fa-check text-green-600"></i>
</div>
<div>
<p class="text-sm font-medium text-white">Dashboard initialized</p>
<p class="text-xs text-gray-400">Just now - System ready</p>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="mt-8 text-center text-sm text-gray-400">
<p>Home Server Dashboard v1.0.0 | Last updated: <span id="last-updated">Just now</span></p>
<p class="mt-1">© 2023 Home Server | <a href="#" class="text-indigo-600 hover:text-indigo-400">Server Settings</a></p>
</footer>
</div>
<script>
// API endpoints
const API_BASE = window.location.origin;
const REFRESH_INTERVAL = 30; // seconds
// Show notification
function showNotification(message, type = 'info') {
const colors = {
success: 'bg-green-500',
error: 'bg-red-500',
warning: 'bg-yellow-500',
info: 'bg-blue-500'
};
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 ${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg z-50`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// Load system information
async function loadSystemInfo() {
try {
const response = await fetch(`${API_BASE}/api/system`);
const data = await response.json();
if (data.cpu_percent !== undefined) {
const cpuPercent = Math.round(data.cpu_percent);
document.getElementById('cpu-usage').textContent = `${cpuPercent}%`;
document.getElementById('cpu-bar').style.width = `${cpuPercent}%`;
}
if (data.memory_percent !== undefined) {
const memoryPercent = Math.round(data.memory_percent);
document.getElementById('memory-usage').textContent = `${memoryPercent}%`;
document.getElementById('memory-bar').style.width = `${memoryPercent}%`;
}
if (data.disk_percent !== undefined) {
const diskPercent = Math.round(data.disk_percent);
document.getElementById('disk-usage').textContent = `${diskPercent}%`;
document.getElementById('disk-bar').style.width = `${diskPercent}%`;
}
if (data.uptime) {
document.getElementById('server-uptime').textContent = data.uptime;
}
} catch (error) {
console.error('Error loading system info:', error);
}
}
// Create application card
function createAppCard(app) {
const card = document.createElement('div');
card.className = 'bg-gray-700 rounded-lg p-4 border border-gray-600 card-hover';
const statusColor = app.status === 'running' ? 'text-green-400' : 'text-red-400';
const statusIndicator = app.status === 'running' ? 'bg-green-500' : 'bg-red-500';
card.innerHTML = `
<div class="flex justify-between items-start mb-3">
<div class="flex items-center">
<div class="h-10 w-10 rounded-md bg-${app.color || 'gray'}-600 flex items-center justify-center mr-3">
<i class="${app.icon || 'fas fa-cube'} text-white"></i>
</div>
<div>
<h3 class="font-semibold text-white">${app.display_name || app.container_name}</h3>
<p class="text-xs text-gray-400">${app.description || app.image}</p>
</div>
</div>
<div class="flex items-center space-x-2">
<div class="w-2 h-2 rounded-full ${statusIndicator} ${app.status === 'running' ? 'status-indicator' : ''}"></div>
<span class="text-xs ${statusColor}">${app.status}</span>
</div>
</div>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-400">Uptime:</span>
<span class="text-white">${app.uptime || 'N/A'}</span>
</div>
<div class="flex space-x-2">
${app.url ? `<a href="${app.url}" target="_blank" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white text-sm py-1 px-3 rounded text-center">
<i class="fas fa-external-link-alt mr-1"></i>Open
</a>` : ''}
<button onclick="editApplication('${app.container_id}')" class="bg-gray-600 hover:bg-gray-500 text-white text-sm py-1 px-3 rounded">
<i class="fas fa-edit mr-1"></i>Edit
</button>
</div>
</div>
`;
return card;
}
// Load applications from database
async function loadApplications() {
try {
const response = await fetch(`${API_BASE}/api/applications`);
const data = await response.json();
const containerGrid = document.getElementById('container-grid');
containerGrid.innerHTML = '';
if (data.applications && data.applications.length > 0) {
document.getElementById('app-count').textContent = `${data.applications.length} apps`;
data.applications.forEach(app => {
const card = createAppCard(app);
containerGrid.appendChild(card);
});
} else {
containerGrid.innerHTML = '<div class="col-span-2 text-center py-8"><i class="fas fa-inbox text-4xl text-gray-400 mb-2"></i><p class="text-gray-400">No applications found</p><button onclick="syncApplications()" class="mt-4 bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded">Sync Applications</button></div>';
}
} catch (error) {
console.error('Error loading applications:', error);
showNotification('Error loading applications', 'error');
}
}
// Edit application
async function editApplication(containerId) {
try {
const response = await fetch(`${API_BASE}/api/applications/${containerId}`);
const app = await response.json();
if (app.error) {
showNotification('Application not found', 'error');
return;
}
const newName = prompt('Enter display name:', app.display_name || app.container_name);
const newIcon = prompt('Enter icon class (e.g., fas fa-cube):', app.icon || 'fas fa-cube');
const newColor = prompt('Enter color (e.g., blue, green, red):', app.color || 'gray');
const newDescription = prompt('Enter description:', app.description || '');
const newUrl = prompt('Enter URL:', app.url || '');
if (newName && newIcon && newColor) {
const updateResponse = await fetch(`${API_BASE}/api/applications/${containerId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
display_name: newName,
icon: newIcon,
color: newColor,
description: newDescription,
url: newUrl
})
});
const result = await updateResponse.json();
if (result.success) {
showNotification('Application updated successfully', 'success');
loadApplications();
} else {
showNotification('Failed to update application', 'error');
}
}
} catch (error) {
console.error('Error editing application:', error);
showNotification('Error editing application', 'error');
}
}
// Sync applications with containers
async function syncApplications() {
try {
const response = await fetch(`${API_BASE}/api/applications/sync`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
showNotification('Applications synced successfully', 'success');
loadApplications();
} else {
showNotification('Failed to sync applications', 'error');
}
} catch (error) {
console.error('Error syncing applications:', error);
showNotification('Error syncing applications', 'error');
}
}
// Refresh all data
async function refreshData() {
const indicator = document.getElementById('refresh-indicator');
indicator.classList.remove('hidden');
await loadSystemInfo();
await loadApplications();
indicator.classList.add('hidden');
showNotification('Data refreshed', 'success');
}
// Update timestamps
function updateTimestamps() {
const now = new Date();
document.getElementById('last-updated').textContent = now.toLocaleString();
}
// Initialize dashboard
async function initDashboard() {
await loadSystemInfo();
await loadApplications();
updateTimestamps();
showNotification('Dashboard loaded successfully', 'success');
}
// Start dashboard
initDashboard();
// Auto refresh
setInterval(() => {
loadSystemInfo();
loadApplications();
updateTimestamps();
}, REFRESH_INTERVAL * 1000);
// Handle connection events
window.addEventListener('online', () => {
showNotification('Connection restored', 'success');
initDashboard();
});
window.addEventListener('offline', () => {
showNotification('Connection lost', 'error');
});
</script>
</body>
</html>