Initial homelab infrastructure

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-13 19:41:46 +02:00
co-authored by Cursor
commit 5014d55d62
31 changed files with 1594 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
# Copy to .env and fill in. Never commit .env to git.
# Let's Encrypt / Traefik
ACME_EMAIL=
# Pi-hole admin password
PIHOLE_PASSWORD=
# Traefik basic auth generate: htpasswd -nb username password
# Then add to traefik/dynamic.yml middlewares.auth.basicAuth.users
TRAEFIK_AUTH_USERS=
# WOL API & Dashboard
WOL_API_USER=
WOL_API_PASS=
# WEATHER_API_KEY= # OpenWeatherMap optional for dashboard
# WEATHER_CITY=
# WOL_DEVICES=desktop:AA:BB:CC:DD:EE:FF:192.168.1.10 (name:mac:ip)
# WOL_BROADCAST_IP=192.168.0.255
# Drone CI (Gitea OAuth app, redirect https://drone.zea.lt/login)
DRONE_GITEA_CLIENT_ID=
DRONE_GITEA_CLIENT_SECRET=
DRONE_RPC_SECRET=
DRONE_ADMIN_USER=
# CUPS printer admin
CUPSADMIN=admin
CUPSPASSWORD=
+30
View File
@@ -0,0 +1,30 @@
# Sensitive data never commit
.env
traefik/letsencrypt/
traefik/dynamic.yml
pihole/etc-pihole/
pihole/etc-dnsmasq.d/
wireguard/config/
cups/config/
# Data dirs keep docker-compose.yml, ignore runtime data
gitea/data/
gitea/indexers/
gitea/log/
gitea/ssh/
gitea/gitea/
gitea/git/
registry/*
!registry/docker-compose.yml
drone/*
!drone/docker-compose.yml
uptime-kuma/*
!uptime-kuma/docker-compose.yml
# Backups (may contain old secrets)
*.bak
docker-compose.override.yml
# Logs & temp
*.log
.DS_Store
+246
View File
@@ -0,0 +1,246 @@
# Homelab Additions Plan: Kuma, Watchtower, Drone CI, CUPS Printer
## Overview
Add four new capabilities to the homelab:
1. **Uptime Kuma** Monitor service availability (HTTP, ping, etc.)
2. **Watchtower** Auto-update container images
3. **Drone CI** CI/CD for Gitea repos (server + Docker runner)
4. **CUPS** USB printer sharing for remote printing
---
## 1. Uptime Kuma
- **Image:** `louislam/uptime-kuma:1`
- **Port:** 3001
- **Data:** `./uptime-kuma:/app/data`
- **Traefik:** `kuma.zea.lt` with basic auth (same as other admin services)
- **Optional:** Mount `/var/run/docker.sock` for container monitoring
---
## 2. Watchtower
**Note:** Watchtower [is no longer maintained](https://github.com/containrrr/watchtower/discussions/2135). Alternatives:
- **WUD (What's Up Docker)** Web UI, manual/controlled updates, notifications
- **Diun** Notifications only (you decide when to update)
If you still want Watchtower for automatic updates, it works but may not receive future fixes.
- **Image:** `containrrr/watchtower`
- **Config:** Mount `/var/run/docker.sock`, run with `--schedule "0 0 4 * * *"` (daily at 4 AM) or `--interval 86400`
- **Optional:** `WATCHTOWER_CLEANUP=true` to remove old images
- **Label:** Add `com.centurylinklabs.watchtower.enable=false` to services you want to exclude (e.g. Traefik, WireGuard)
---
## 3. Drone CI
Requires **Gitea OAuth app** (manual step before starting Drone):
1. In Gitea: **Settings → Applications → Create OAuth2 Application**
2. **Application Name:** Drone
3. **Redirect URI:** `https://drone.zea.lt/login`
4. **Confidential Client:** checked
5. Save and copy **Client ID** and **Client Secret**
Components:
### Drone Server
- **Image:** `drone/drone:2`
- **Database:** SQLite in `/data` (simplest for homelab)
- **Env:** `DRONE_GITEA_*`, `DRONE_RPC_SECRET`, `DRONE_SERVER_HOST`, `DRONE_SERVER_PROTO`
- **Traefik:** `drone.zea.lt`
### Drone Docker Runner
- **Image:** `drone/drone-runner-docker:1`
- **Volume:** `/var/run/docker.sock`
- **Env:** `DRONE_RPC_HOST=drone`, `DRONE_RPC_PROTO=http`, `DRONE_RPC_SECRET=<same as server>`
- **Network:** `homelab` (to reach Gitea, Registry)
**Critical:** `DRONE_GITEA_SERVER` must be reachable from the Drone server. Use `https://git.zea.lt` (external URL) for OAuth; internal `http://gitea:3000` can cause callback issues when Gitea redirects.
---
## 4. CUPS (USB Printer via Docker)
- **Image:** `infra7/cups:latest` (supports arm64 for Raspberry Pi)
- **Device:** `--device /dev/bus/usb`
- **Ports:** `631:631` (IPP + web admin)
- **Env:** `CUPSADMIN`, `CUPSPASSWORD` (or Docker secrets)
- **Volume:** `./cups/config:/etc/cups` for persistence
- **Traefik:** `print.zea.lt` for web admin (optional)
**Remote printing:**
- Clients add printer: `ipp://YOUR_SERVER_IP:631/printers/<printer-name>`
- Or via `print.zea.lt` if you proxy IPP (less common)
**USB setup:**
1. Plug in printer
2. Run `lsusb` on host to verify
3. Ensure Docker can access `/dev/bus/usb` (permissions)
4. Open `http://YOUR_SERVER_IP:631` or `https://print.zea.lt` and add printer
---
## DNS Updates
Add to `pihole/etc-dnsmasq.d/02-custom.conf`:
```
address=/kuma.zea.lt/YOUR_SERVER_IP
address=/drone.zea.lt/YOUR_SERVER_IP
address=/print.zea.lt/YOUR_SERVER_IP
```
---
## Docker Compose Additions (draft)
```yaml
### 📊 Uptime Kuma ###
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
restart: always
volumes:
- ./uptime-kuma:/app/data
- /var/run/docker.sock:/var/run/docker.sock # optional: monitor containers
networks:
- homelab
labels:
- "traefik.enable=true"
- "traefik.http.routers.kuma.rule=Host(`kuma.zea.lt`)"
- "traefik.http.routers.kuma.entrypoints=websecure"
- "traefik.http.routers.kuma.tls.certresolver=leresolver"
- "traefik.http.routers.kuma.middlewares=auth"
- "traefik.http.services.kuma.loadbalancer.server.port=3001"
### 🔄 Watchtower ###
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_SCHEDULE=0 0 4 * * *
- WATCHTOWER_CLEANUP=true
networks:
- homelab
### 🚀 Drone CI ###
drone-server:
image: drone/drone:2
container_name: drone
restart: always
environment:
- DRONE_GITEA_SERVER=https://git.zea.lt
- DRONE_GITEA_CLIENT_ID=${DRONE_GITEA_CLIENT_ID}
- DRONE_GITEA_CLIENT_SECRET=${DRONE_GITEA_CLIENT_SECRET}
- DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
- DRONE_SERVER_HOST=drone.zea.lt
- DRONE_SERVER_PROTO=https
- DRONE_USER_CREATE=username:YOUR_GITEA_USER,admin:true # first admin
volumes:
- ./drone:/data
networks:
- homelab
labels:
- "traefik.enable=true"
- "traefik.http.routers.drone.rule=Host(`drone.zea.lt`)"
- "traefik.http.routers.drone.entrypoints=websecure"
- "traefik.http.routers.drone.tls.certresolver=leresolver"
- "traefik.http.routers.drone.middlewares=auth"
- "traefik.http.services.drone.loadbalancer.server.port=80"
drone-runner:
image: drone/drone-runner-docker:1
container_name: drone-runner
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DRONE_RPC_HOST=drone
- DRONE_RPC_PROTO=http
- DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
- DRONE_RUNNER_CAPACITY=2
networks:
- homelab
### 🖨️ CUPS Printer Server ###
cups:
image: infra7/cups:latest
container_name: cups
restart: always
ulimits:
nofile:
soft: 65536
hard: 65536
devices:
- /dev/bus/usb:/dev/bus/usb
environment:
- TZ=Europe/London
- CUPSADMIN=${CUPSADMIN:-admin}
- CUPSPASSWORD=${CUPSPASSWORD}
volumes:
- ./cups/config:/etc/cups
ports:
- "631:631"
networks:
- homelab
labels:
- "traefik.enable=true"
- "traefik.http.routers.print.rule=Host(`print.zea.lt`)"
- "traefik.http.routers.print.entrypoints=websecure"
- "traefik.http.routers.print.tls.certresolver=leresolver"
- "traefik.http.services.print.loadbalancer.server.port=631"
```
---
## .env Additions
```
# Drone CI (create OAuth app in Gitea first)
DRONE_GITEA_CLIENT_ID=your-client-id
DRONE_GITEA_CLIENT_SECRET=your-client-secret
DRONE_RPC_SECRET=<openssl rand -hex 16>
# CUPS printer admin
CUPSADMIN=admin
CUPSPASSWORD=your-secure-password
```
---
## Implementation Order
1. Add DNS entries for `kuma.zea.lt`, `drone.zea.lt`, `print.zea.lt`
2. Create directories: `mkdir -p uptime-kuma drone cups/config`
3. Add services to `docker-compose.yml`
4. Add secrets to `.env`
5. Create Gitea OAuth app
6. `docker compose up -d` for new services
7. Plug in USB printer, access CUPS at `https://print.zea.lt` or `:631`, add printer
8. Open Drone at `https://drone.zea.lt`, activate repos
9. Configure Uptime Kuma monitors for your services
---
## Watchtower Exclusions (optional)
To prevent Watchtower from updating critical services (Traefik, WireGuard), add:
```yaml
labels:
- "com.centurylinklabs.watchtower.enable=false"
```
To services: `traefik`, `wireguard`, optionally `pihole`.
+20
View File
@@ -0,0 +1,20 @@
# Secrets Checklist Never Commit
Before pushing to git, ensure these are **only** in `.env` (gitignored):
| Variable | Used by | Example |
|----------|---------|---------|
| `ACME_EMAIL` | Traefik | Your email for Let's Encrypt |
| `PIHOLE_PASSWORD` | Pi-hole | Web UI password |
| `TRAEFIK_AUTH_USERS` | traefik/dynamic.yml | `htpasswd -nb user pass` |
| `WOL_API_USER` / `WOL_API_PASS` | Dashboard, WOL API | Basic auth |
| `WOL_DEVICES` | WOL API | `desktop:MAC:IP` format |
| `DRONE_GITEA_CLIENT_ID` / `SECRET` | Drone | OAuth from Gitea |
| `DRONE_RPC_SECRET` | Drone | `openssl rand -hex 16` |
| `DRONE_ADMIN_USER` | Drone | Your Gitea username |
| `CUPSADMIN` / `CUPSPASSWORD` | CUPS | Printer admin |
| `WEATHER_API_KEY` / `WEATHER_CITY` | Dashboard | OpenWeatherMap (optional) |
**Files always gitignored:** `.env`, `traefik/dynamic.yml`, `traefik/letsencrypt/`, data dirs (gitea, pihole, etc.)
**Verify before push:** `git status` `.env` and `traefik/dynamic.yml` must not appear.
+27
View File
@@ -0,0 +1,27 @@
services:
cups:
image: infra7/cups:latest
container_name: cups
restart: always
ulimits:
nofile:
soft: 65536
hard: 65536
devices:
- /dev/bus/usb:/dev/bus/usb
environment:
- TZ=Europe/London
- CUPSADMIN=${CUPSADMIN:-admin}
- CUPSPASSWORD=${CUPSPASSWORD}
volumes:
- ./config:/etc/cups
ports:
- "6363:631"
networks:
- homelab
labels:
- "traefik.enable=true"
- "traefik.http.routers.print.rule=Host(`print.zea.lt`)"
- "traefik.http.routers.print.entrypoints=websecure"
- "traefik.http.routers.print.tls.certresolver=leresolver"
- "traefik.http.services.print.loadbalancer.server.port=631"
+13
View File
@@ -0,0 +1,13 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["python", "app.py"]
+116
View File
@@ -0,0 +1,116 @@
from flask import Flask, render_template, jsonify, request, Response
import requests
import os
from functools import wraps
from requests.auth import HTTPBasicAuth
app = Flask(__name__)
WOL_API_URL = os.getenv("WOL_API_URL", "https://wol.zea.lt")
WOL_API_USER = os.getenv("WOL_API_USER")
WOL_API_PASS = os.getenv("WOL_API_PASS")
WEATHER_API_KEY = os.getenv("WEATHER_API_KEY")
WEATHER_CITY = os.getenv("WEATHER_CITY", "")
def _fetch_wol_status():
"""Fetch device list and status from WOL API (single source of truth)."""
try:
auth = (
HTTPBasicAuth(WOL_API_USER, WOL_API_PASS)
if WOL_API_USER and WOL_API_PASS
else None
)
r = requests.get(
f"{WOL_API_URL}/status",
auth=auth,
timeout=5,
)
if r.ok:
return r.json().get("devices", {})
except requests.RequestException:
pass
return {}
def check_auth(username, password):
return username == WOL_API_USER and password == WOL_API_PASS
def authenticate():
"""Sends a 401 response that enables basic auth."""
return Response(
"Could not verify your access level for that URL.\n"
"You have to login with proper credentials",
401,
{"WWW-Authenticate": 'Basic realm="Login Required"'},
)
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
@app.route("/")
def dashboard():
devices_data = _fetch_wol_status()
devices = {k: v.get("mac", "") for k, v in devices_data.items()}
status = {k: v.get("online", False) for k, v in devices_data.items()}
return render_template(
"dashboard.html",
devices=devices,
status=status,
)
@app.route("/wake/<device>", methods=["POST"])
@requires_auth
def wake_device(device):
devices_data = _fetch_wol_status()
if device not in devices_data:
return jsonify({"error": "Device not found"}), 404
try:
auth = HTTPBasicAuth(WOL_API_USER, WOL_API_PASS)
r = requests.post(
f"{WOL_API_URL}/wake/{device}",
auth=auth,
timeout=5,
)
if r.ok:
return jsonify({"message": f"Wake command sent to {device}"})
return jsonify({"error": f"Failed to send wake command: {r.text}"}), r.status_code
except requests.RequestException as e:
return jsonify({"error": str(e)}), 500
@app.route("/weather")
def weather():
"""Proxy for OpenWeatherMap keeps API key server-side."""
if not WEATHER_API_KEY or not WEATHER_CITY:
return jsonify({"error": "Weather not configured"}), 503
try:
r = requests.get(
"https://api.openweathermap.org/data/2.5/weather",
params={
"q": WEATHER_CITY,
"appid": WEATHER_API_KEY,
"units": "metric",
},
timeout=5,
)
if r.ok:
return jsonify(r.json())
except requests.RequestException:
pass
return jsonify({"error": "Weather unavailable"}), 503
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
+21
View File
@@ -0,0 +1,21 @@
services:
dashboard:
build: .
container_name: dashboard
restart: always
environment:
- WOL_API_URL=${WOL_API_URL:-https://wol.zea.lt}
- WOL_API_USER=${WOL_API_USER}
- WOL_API_PASS=${WOL_API_PASS}
- WEATHER_API_KEY=${WEATHER_API_KEY}
- WEATHER_CITY=${WEATHER_CITY}
networks:
- homelab
ports:
- "8080:8080"
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`zea.lt`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=leresolver"
- "traefik.http.routers.dashboard.middlewares=auth@file"
+2
View File
@@ -0,0 +1,2 @@
Flask==2.3.2
requests==2.31.0
+197
View File
@@ -0,0 +1,197 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>HomeLab Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet" />
<style>
html, body {
height: 100%;
margin: 0;
background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
color: #fff;
font-family: 'Segoe UI', sans-serif;
}
.container {
min-height: 100vh;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 2rem;
}
.clock {
font-size: 5rem;
font-weight: bold;
text-align: center;
}
.date {
font-size: 1.5rem;
text-align: center;
}
.weather {
text-align: center;
font-size: 1.2rem;
}
input[type="search"] {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: none;
margin: 1rem auto 2rem;
font-size: 1.1rem;
max-width: 600px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1.5rem;
}
.device-card, .service-card {
background: rgba(255, 255, 255, 0.06);
border-radius: 1rem;
padding: 1.25rem;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
transition: transform 0.2s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.device-card:hover, .service-card:hover {
transform: scale(1.03);
background: rgba(255, 255, 255, 0.09);
}
.device-status {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.btn-wake {
padding: 0.5rem 1.25rem;
font-size: 0.95rem;
border-radius: 0.5rem;
margin-top: 0.5rem;
}
a {
color: #0dcaf0;
text-decoration: none;
font-size: 1.1rem;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="clock" id="clock"></div>
<div class="date" id="date"></div>
<div class="weather" id="weather">Loading weather...</div>
<input type="search" id="search" placeholder="Search Google or type a URL..." onkeydown="searchInput(event)" />
<section>
<h2>Services</h2>
<div class="card-grid">
<div class="service-card"><a href="https://git.zea.lt">🗂️ Gitea</a></div>
<div class="service-card"><a href="https://registry.zea.lt">📦 Docker Registry</a></div>
<div class="service-card"><a href="https://traefik.zea.lt">📊 Traefik Dashboard</a></div>
<div class="service-card"><a href="https://wol.zea.lt">🖲️ Wake-on-LAN API</a></div>
<div class="service-card"><a href="https://kuma.zea.lt">📈 Uptime Kuma</a></div>
<div class="service-card"><a href="https://drone.zea.lt">🚀 Drone CI</a></div>
<div class="service-card"><a href="https://print.zea.lt">🖨️ Printer (CUPS)</a></div>
</div>
</section>
<section>
<h2>Devices</h2>
<div class="card-grid">
{% for dev, mac in devices.items() %}
<div class="device-card">
<div class="device-status">
{% if status[dev] %}
🟢
{% else %}
🔴
{% endif %}
</div>
<h5>{{ dev|capitalize }}</h5>
<button
class="btn btn-{{ 'secondary' if status[dev] else 'primary' }} btn-wake"
{% if status[dev] %}disabled{% endif %}
onclick="wake('{{ dev }}')">
Wake
</button>
</div>
{% endfor %}
</div>
</section>
</div>
<script>
// Clock & date
function updateClock() {
const now = new Date();
const clock = now.toLocaleTimeString();
const date = now.toLocaleDateString(undefined, {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
});
document.getElementById('clock').textContent = clock;
document.getElementById('date').textContent = date;
}
setInterval(updateClock, 1000);
updateClock();
// Weather (fetched via server proxy API key stays server-side)
async function fetchWeather() {
const res = await fetch('/weather');
if (!res.ok) return document.getElementById('weather').textContent = 'Weather unavailable';
const data = await res.json();
if (data.error) return document.getElementById('weather').textContent = 'Weather unavailable';
const temp = data.main.temp.toFixed(1);
const desc = data.weather[0].description;
const icon = data.weather[0].icon;
const city = data.name;
document.getElementById('weather').innerHTML =
`<img src="https://openweathermap.org/img/wn/${icon}.png" alt="" /> ${city}: ${temp}°C, ${desc}`;
}
fetchWeather();
// Wake device
async function wake(device) {
const resp = await fetch(`/wake/${device}`, { method: 'POST' });
const data = await resp.json();
alert(data.message || data.error);
location.reload();
}
// Smart search
function searchInput(e) {
if (e.key === 'Enter') {
let q = e.target.value.trim();
if (!q) return;
if (!q.startsWith('http') && q.includes('.') && !q.includes(' ')) {
if (!q.startsWith('http://') && !q.startsWith('https://')) q = 'http://' + q;
location.href = q;
} else {
location.href = 'https://www.google.com/search?q=' + encodeURIComponent(q);
}
}
}
</script>
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
# Homelab aggregator runs all per-folder compose files
#
# Usage:
# From root (all services): docker compose up -d
# Single folder: cd traefik && docker compose up -d
# (create network first: docker network create homelab)
#
# Requires: Docker Compose v2.20+ (include support)
# Network "homelab" is created by traefik (must start traefik first for others)
include:
- path: traefik/docker-compose.yml
- path: gitea/docker-compose.yml
- path: registry/docker-compose.yml
- path: wol-api/docker-compose.yml
- path: pihole/docker-compose.yml
- path: wireguard/docker-compose.yml
- path: dashboard/docker-compose.yml
- path: uptime-kuma/docker-compose.yml
- path: watchtower/docker-compose.yml
- path: drone/docker-compose.yml
- path: cups/docker-compose.yml
+38
View File
@@ -0,0 +1,38 @@
services:
drone:
image: drone/drone:2
container_name: drone
restart: always
environment:
- DRONE_GITEA_SERVER=https://git.zea.lt
- DRONE_GITEA_CLIENT_ID=${DRONE_GITEA_CLIENT_ID}
- DRONE_GITEA_CLIENT_SECRET=${DRONE_GITEA_CLIENT_SECRET}
- DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
- DRONE_SERVER_HOST=drone.zea.lt
- DRONE_SERVER_PROTO=https
- DRONE_USER_CREATE=username:${DRONE_ADMIN_USER},admin:true
volumes:
- .:/data
networks:
- homelab
labels:
- "traefik.enable=true"
- "traefik.http.routers.drone.rule=Host(`drone.zea.lt`)"
- "traefik.http.routers.drone.entrypoints=websecure"
- "traefik.http.routers.drone.tls.certresolver=leresolver"
- "traefik.http.routers.drone.middlewares=auth@file"
- "traefik.http.services.drone.loadbalancer.server.port=80"
drone-runner:
image: drone/drone-runner-docker:1
container_name: drone-runner
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DRONE_RPC_HOST=drone
- DRONE_RPC_PROTO=http
- DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
- DRONE_RUNNER_CAPACITY=2
networks:
- homelab
+19
View File
@@ -0,0 +1,19 @@
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: always
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__server__ROOT_URL=https://git.zea.lt
volumes:
- .:/data
networks:
- homelab
labels:
- "traefik.enable=true"
- "traefik.http.routers.gitea.rule=Host(`git.zea.lt`)"
- "traefik.http.routers.gitea.entrypoints=websecure"
- "traefik.http.routers.gitea.tls.certresolver=leresolver"
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
+22
View File
@@ -0,0 +1,22 @@
services:
pihole:
image: pihole/pihole:latest
container_name: pihole
restart: always
network_mode: host
environment:
- TZ=Europe/London
- WEBPASSWORD=${PIHOLE_PASSWORD:-admin}
- WEBPORT=8080
- DNSMASQ_LISTENING=all
volumes:
- ./etc-pihole:/etc/pihole
- ./etc-dnsmasq.d:/etc/dnsmasq.d
labels:
- "traefik.enable=true"
- "traefik.http.routers.pihole.rule=Host(`pihole.zea.lt`)"
- "traefik.http.routers.pihole.entrypoints=websecure"
- "traefik.http.routers.pihole.tls.certresolver=leresolver"
- "traefik.http.routers.pihole.middlewares=pihole-prefix"
- "traefik.http.middlewares.pihole-prefix.addprefix.prefix=/admin"
- "traefik.http.services.pihole.loadbalancer.server.port=8080"
+14
View File
@@ -0,0 +1,14 @@
services:
registry:
image: registry:2
container_name: registry
restart: always
volumes:
- .:/var/lib/registry
networks:
- homelab
labels:
- "traefik.enable=true"
- "traefik.http.routers.registry.rule=Host(`registry.zea.lt`)"
- "traefik.http.routers.registry.entrypoints=websecure"
- "traefik.http.routers.registry.tls.certresolver=leresolver"
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
# Configure homelab repo to push to both Gitea (local) and remote (e.g. GitHub)
# Run once after: git init && git add . && git commit -m "Initial homelab config"
#
# Usage: ./setup-git-dual-remote.sh [gitea_url] [remote_url]
# Example: ./setup-git-dual-remote.sh https://git.zea.lt/USER/homelab.git https://github.com/USER/homelab.git
set -e
GITEA_URL="${1:?Usage: $0 <gitea_repo_url> <remote_repo_url>}"
REMOTE_URL="${2:?Usage: $0 <gitea_repo_url> <remote_repo_url>}"
cd "$(dirname "$0")"
if [[ ! -d .git ]]; then
echo "Initializing git..."
git init
git add .
git commit -m "Initial homelab infrastructure" || true
fi
# Remove default origin if it exists (e.g. from clone)
git remote remove origin 2>/dev/null || true
# Add origin with dual push URLs one push goes to both
git remote add origin "$GITEA_URL"
git remote set-url --add --push origin "$GITEA_URL"
git remote set-url --add --push origin "$REMOTE_URL"
echo "Dual remote configured."
echo " Local (Gitea): $GITEA_URL"
echo " Remote backup: $REMOTE_URL"
echo ""
echo "Create both repos first (empty, no readme). Then:"
echo " git branch -M main"
echo " git push -u origin main"
+161
View File
@@ -0,0 +1,161 @@
#!/bin/bash
set -e
BASE_DIR="$HOME/homelab"
DASHBOARD_DIR="$BASE_DIR/dashboard"
TEMPLATES_DIR="$DASHBOARD_DIR/templates"
DOCKER_COMPOSE_FILE="$BASE_DIR/docker-compose.yml"
echo "Creating dashboard directory structure..."
mkdir -p "$TEMPLATES_DIR"
echo "Writing app.py..."
cat > "$DASHBOARD_DIR/app.py" << 'EOF'
from flask import Flask, render_template, jsonify, request
import subprocess
import requests
app = Flask(__name__)
DEVICES = {
"desktop": "AA:BB:CC:DD:EE:FF", # Replace with real MAC
}
DEVICE_IPS = {
"desktop": "192.168.1.10", # Replace with real IP
}
WOL_API_URL = "https://wol.zea.lt"
def ping_host(ip):
try:
subprocess.check_output(['ping', '-c', '1', '-W', '1', ip])
return True
except subprocess.CalledProcessError:
return False
@app.route('/')
def dashboard():
status = {dev: ping_host(ip) for dev, ip in DEVICE_IPS.items()}
return render_template('dashboard.html', devices=DEVICES, status=status)
@app.route('/wake/<device>', methods=['POST'])
def wake_device(device):
if device not in DEVICES:
return jsonify({"error": "Device not found"}), 404
try:
r = requests.post(f"{WOL_API_URL}/wake/{device}")
if r.ok:
return jsonify({"message": f"Wake command sent to {device}"})
else:
return jsonify({"error": f"Failed to send wake command: {r.text}"}), r.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
EOF
echo "Writing dashboard.html template..."
cat > "$TEMPLATES_DIR/dashboard.html" << 'EOF'
<!doctype html>
<html lang="en">
<head>
<title>HomeLab Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body class="p-4">
<h1>HomeLab Dashboard</h1>
<h2>Services</h2>
<ul>
<li><a href="https://git.zea.lt" target="_blank">Gitea</a></li>
<li><a href="https://registry.zea.lt" target="_blank">Docker Registry</a></li>
<li><a href="https://traefik.zea.lt" target="_blank">Traefik Dashboard</a></li>
<li><a href="https://wol.zea.lt" target="_blank">Wake-on-LAN API</a></li>
</ul>
<h2>Devices</h2>
<table class="table table-bordered" style="max-width:600px;">
<thead>
<tr>
<th>Device</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for device, mac in devices.items() %}
<tr>
<td>{{ device }}</td>
<td>
{% if status[device] %}
<span class="badge bg-success">Online</span>
{% else %}
<span class="badge bg-danger">Offline</span>
{% endif %}
</td>
<td>
<button class="btn btn-primary btn-sm" onclick="wake('{{ device }}')">Wake</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<script>
async function wake(device) {
const resp = await fetch(`/wake/${device}`, { method: 'POST' });
const data = await resp.json();
alert(data.message || data.error);
}
</script>
</body>
</html>
EOF
echo "Writing Dockerfile..."
cat > "$DASHBOARD_DIR/Dockerfile" << 'EOF'
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["python", "app.py"]
EOF
echo "Writing requirements.txt..."
cat > "$DASHBOARD_DIR/requirements.txt" << 'EOF'
Flask==2.3.2
requests==2.31.0
EOF
# Backup docker-compose.yml just in case
cp "$DOCKER_COMPOSE_FILE" "$DOCKER_COMPOSE_FILE.bak"
echo "Updating docker-compose.yml to add dashboard service..."
# Append dashboard service before the last line if it ends with '...' or just at end
# If docker-compose.yml uses version 3+ it should be fine
cat >> "$DOCKER_COMPOSE_FILE" << 'EOF'
dashboard:
build: ./dashboard
container_name: dashboard
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`dashboard.zea.lt`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=leresolver"
EOF
echo "Setup complete! You can now run:"
echo "cd $BASE_DIR && docker-compose build dashboard && docker-compose up -d dashboard"
+200
View File
@@ -0,0 +1,200 @@
#!/bin/bash
# Homelab Setup Script
echo "🏠 Setting up homelab directories and files..."
# Create main directories
mkdir -p traefik/letsencrypt
mkdir -p gitea
mkdir -p registry
mkdir -p wol-api
mkdir -p pihole/etc-pihole
mkdir -p pihole/etc-dnsmasq.d
mkdir -p wireguard/config
# Set proper permissions
chmod 600 traefik/letsencrypt
chmod 755 gitea registry pihole/etc-pihole pihole/etc-dnsmasq.d wireguard/config
# Create .env file
cat > .env << 'EOF'
# Pi-hole admin password
PIHOLE_PASSWORD=your_secure_password_here
# Change this to a secure password!
EOF
# Create basic traefik.yml config
cat > traefik/traefik.yml << 'EOF'
# Static configuration
api:
dashboard: true
insecure: false
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
certificatesResolvers:
leresolver:
acme:
httpChallenge:
entryPoint: web
email: your-email@example.com
storage: /letsencrypt/acme.json
EOF
# Create basic WOL API Dockerfile
cat > wol-api/Dockerfile << 'EOF'
FROM python:3.9-slim
WORKDIR /app
# Install wake-on-lan package
RUN pip install flask wakeonlan
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
EOF
# Create basic WOL API application
cat > wol-api/app.py << 'EOF'
from flask import Flask, request, jsonify
from wakeonlan import send_magic_packet
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
# Define your devices here
DEVICES = {
"desktop": "00:11:22:33:44:55", # Replace with actual MAC addresses
"laptop": "AA:BB:CC:DD:EE:FF",
# Add more devices as needed
}
@app.route('/wake/<device>', methods=['POST'])
def wake_device(device):
if device not in DEVICES:
return jsonify({"error": "Device not found"}), 404
try:
mac_address = DEVICES[device]
send_magic_packet(mac_address)
app.logger.info(f"Magic packet sent to {device} ({mac_address})")
return jsonify({"message": f"Wake-on-LAN packet sent to {device}"}), 200
except Exception as e:
app.logger.error(f"Error sending wake packet: {e}")
return jsonify({"error": "Failed to send wake packet"}), 500
@app.route('/devices', methods=['GET'])
def list_devices():
return jsonify({"devices": list(DEVICES.keys())})
@app.route('/health', methods=['GET'])
def health_check():
return jsonify({"status": "healthy"}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
EOF
# Create Pi-hole custom DNS config
cat > pihole/etc-dnsmasq.d/02-custom.conf << 'EOF'
# Allow queries from local network
interface=eth0
listen-address=127.0.0.1,0.0.0.0
bind-interfaces
# Custom DNS entries for your homelab
# Format: address=/domain.name/IP
address=/traefik.zea.lt/192.168.1.100
address=/git.zea.lt/192.168.1.100
address=/registry.zea.lt/192.168.1.100
address=/wol.zea.lt/192.168.1.100
address=/pihole.zea.lt/192.168.1.100
EOF
# Create startup script
cat > start.sh << 'EOF'
#!/bin/bash
echo "🚀 Starting homelab services..."
docker-compose up -d
echo "✅ Services started!"
echo ""
echo "🔗 Access points:"
echo " - Traefik Dashboard: https://traefik.zea.lt"
echo " - Gitea: https://git.zea.lt"
echo " - Pi-hole: https://pihole.zea.lt"
echo " - Docker Registry: https://registry.zea.lt"
echo " - WOL API: https://wol.zea.lt"
echo ""
echo "📋 Next steps:"
echo " 1. Check WireGuard logs: docker logs wireguard"
echo " 2. Get VPN configs from: ./wireguard/config/peer1/"
echo " 3. Update .env with secure passwords"
echo " 4. Update MAC addresses in wol-api/app.py"
echo " 5. Update server IP in pihole/etc-dnsmasq.d/02-custom.conf"
EOF
# Create stop script
cat > stop.sh << 'EOF'
#!/bin/bash
echo "🛑 Stopping homelab services..."
docker-compose down
echo "✅ Services stopped!"
EOF
# Create update script
cat > update.sh << 'EOF'
#!/bin/bash
echo "🔄 Updating homelab services..."
docker-compose pull
docker-compose up -d
echo "✅ Services updated!"
EOF
# Make scripts executable
chmod +x start.sh stop.sh update.sh
# Create .gitignore
cat > .gitignore << 'EOF'
# Sensitive data
.env
traefik/letsencrypt/
gitea/
registry/
pihole/etc-pihole/
wireguard/config/
# Logs
*.log
EOF
echo "✅ Homelab setup complete!"
echo ""
echo "📝 TODO before starting:"
echo " 1. Update .env with secure passwords"
echo " 2. Update MAC addresses in wol-api/app.py"
echo " 3. Update server IP in pihole/etc-dnsmasq.d/02-custom.conf"
echo " 4. Make sure your domains (*.zea.lt) point to your server"
echo ""
echo "🚀 To start: ./start.sh"
echo "🛑 To stop: ./stop.sh"
echo "🔄 To update: ./update.sh"
Executable
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
echo "🚀 Starting homelab services..."
docker compose up -d
echo "✅ Services started!"
echo ""
echo "🔗 Access points:"
echo " - Traefik Dashboard: https://traefik.zea.lt"
echo " - Gitea: https://git.zea.lt"
echo " - Pi-hole: https://pihole.zea.lt"
echo " - Docker Registry: https://registry.zea.lt"
echo " - WOL API: https://wol.zea.lt"
echo " - Uptime Kuma: https://kuma.zea.lt"
echo " - Drone CI: https://drone.zea.lt"
echo " - CUPS Printer: https://print.zea.lt (IPP: port 6363)"
echo ""
echo "📋 Next steps:"
echo " 1. Check WireGuard logs: docker logs wireguard"
echo " 2. Get VPN configs from: ./wireguard/config/peer1/"
echo " 3. Update .env with secure passwords"
echo " 4. Update MAC addresses in wol-api/app.py"
echo " 5. Update server IP in pihole/etc-dnsmasq.d/02-custom.conf"
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
echo "🛑 Stopping homelab services..."
docker compose down
echo "✅ Services stopped!"
+42
View File
@@ -0,0 +1,42 @@
services:
traefik:
image: traefik:v2.11
container_name: traefik
restart: always
command:
- "--api.dashboard=true"
- "--api.insecure=false"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=homelab"
- "--providers.file.filename=/etc/traefik/dynamic.yml"
- "--providers.file.watch=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.leresolver.acme.httpchallenge=true"
- "--certificatesresolvers.leresolver.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.leresolver.acme.email=${ACME_EMAIL}"
- "--certificatesresolvers.leresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
- "./traefik.yml:/traefik.yml:ro"
- "./dynamic.yml:/etc/traefik/dynamic.yml:ro"
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.zea.lt`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=leresolver"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.middlewares=auth@file"
- "com.centurylinklabs.watchtower.enable=false"
networks:
- homelab
networks:
homelab:
name: homelab
driver: bridge
+20
View File
@@ -0,0 +1,20 @@
# Copy to dynamic.yml and add your htpasswd line.
# Generate: htpasswd -nb username password
http:
routers:
traefik-dashboard:
rule: "Host(`traefik.zea.lt`)"
entryPoints:
- websecure
service: api@internal
tls:
certResolver: leresolver
middlewares:
- auth
middlewares:
auth:
basicAuth:
users:
- "USERNAME:$apr1$HASH$YOUR_HASH_HERE"
+29
View File
@@ -0,0 +1,29 @@
# traefik/traefik.yml
api:
dashboard: true
insecure: false
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
filename: /etc/traefik/dynamic.yml
watch: true
certificatesResolvers:
leresolver:
acme:
# Email set via traefik/docker-compose command (ACME_EMAIL env)
email: placeholder@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
echo "🔄 Updating homelab services..."
docker-compose pull
docker-compose up -d
echo "✅ Services updated!"
+57
View File
@@ -0,0 +1,57 @@
# Uptime Kuma Setup Guide
Access Uptime Kuma at **https://kuma.zea.lt** (use Traefik auth: your-username / your-password).
## Why Internal URLs?
Public URLs (https://zea.lt, etc.) are behind Traefik basic auth. Uptime Kuma's health checks get **401 Unauthorized** when using those. Use **internal container URLs** instead—no auth, same reliability.
---
## First-Time Setup
1. Open https://kuma.zea.lt
2. Create your Uptime Kuma admin account (username + password)
3. Click **Add New Monitor** for each service below
---
## Recommended Monitors (Internal URLs No Auth)
| # | Monitor Name | Type | URL/Target | Heartbeat |
|---|--------------|----------|---------------------------------|-----------|
| 1 | Dashboard | HTTP(s) | http://dashboard:8080 | 60 |
| 2 | Gitea | HTTP(s) | http://gitea:3000 | 60 |
| 3 | Docker Registry | HTTP(s) | http://registry:5000 | 60 |
| 4 | Drone CI | HTTP(s) | http://drone:80 | 60 |
| 5 | CUPS Printer | HTTP(s) | http://cups:631 | 300 |
| 6 | WOL API | HTTP(s) | http://host.docker.internal:5000/health | 60 |
| 7 | Pi-hole DNS | TCP Port | host.docker.internal / 53 | 60 |
| 8 | Traefik HTTP | TCP Port | traefik / 80 | 60 |
| 9 | Traefik HTTPS| TCP Port | traefik / 443 | 60 |
|10 | HomeLab Server | Ping | 192.168.1.100 | 60 |
|11 | Desktop PC | Ping | 192.168.1.10 | 60 |
**Note:** For TCP Port monitors (#8, #9), use **Monitor Type: TCP Port**, Host: `traefik`, Port: `80` or `443`.
---
## Step-by-Step (Add Monitor)
1. Click **+ Add New Monitor**
2. **Monitor Type:** HTTP(s) (or TCP Port / Ping)
3. **Friendly Name:** e.g. "Gitea"
4. **URL:** e.g. `http://gitea:3000`
5. **Heartbeat Interval:** 60 (or 300 for CUPS)
6. **Retries:** 2
7. For HTTP(s): **Accepted Status Codes** 200299 (default)
8. Click **Save**
---
## Optional: Notifications
Settings → Notifications → Add:
- **Email** for downtime alerts
- **Telegram** if you use Telegram
- **Webhook** for custom integrations
+19
View File
@@ -0,0 +1,19 @@
services:
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
restart: always
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- .:/app/data
- /var/run/docker.sock:/var/run/docker.sock
networks:
- homelab
labels:
- "traefik.enable=true"
- "traefik.http.routers.kuma.rule=Host(`kuma.zea.lt`)"
- "traefik.http.routers.kuma.entrypoints=websecure"
- "traefik.http.routers.kuma.tls.certresolver=leresolver"
- "traefik.http.routers.kuma.middlewares=auth@file"
- "traefik.http.services.kuma.loadbalancer.server.port=3001"
+12
View File
@@ -0,0 +1,12 @@
services:
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_SCHEDULE=0 0 4 * * *
- WATCHTOWER_CLEANUP=true
networks:
- homelab
+28
View File
@@ -0,0 +1,28 @@
services:
wireguard:
image: linuxserver/wireguard
container_name: wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/London
- SERVERURL=auto
- SERVERPORT=51820
- PEERS=5
- PEERDNS=auto
- INTERNAL_SUBNET=10.13.13.0
volumes:
- ./config:/config
- /lib/modules:/lib/modules
ports:
- "51820:51820/udp"
networks:
- homelab
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
restart: always
labels:
- "com.centurylinklabs.watchtower.enable=false"
+14
View File
@@ -0,0 +1,14 @@
FROM python:3.9-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends iputils-ping \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir flask wakeonlan
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
+113
View File
@@ -0,0 +1,113 @@
from flask import Flask, request, jsonify
from wakeonlan import send_magic_packet
import logging
import os
import subprocess
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
# Format: name:mac:ip,name2:mac2:ip2
# Example: desktop:AA:BB:CC:DD:EE:FF:192.168.1.10
# Set WOL_DEVICES in .env never commit real MACs/IPs
_WOL_DEVICES_RAW = os.getenv("WOL_DEVICES", "")
BROADCAST_IP = os.getenv("WOL_BROADCAST_IP", "255.255.255.255")
WOL_PACKET_COUNT = int(os.getenv("WOL_PACKET_COUNT", "3"))
def _parse_devices() -> tuple[dict[str, str], dict[str, str]]:
"""Parse WOL_DEVICES env into devices (name->mac) and device_ips (name->ip).
Format: name:mac:ip (mac has colons, so use name:XX:XX:XX:XX:XX:XX:ip)
Example: desktop:AA:BB:CC:DD:EE:FF:192.168.1.10
"""
devices: dict[str, str] = {}
device_ips: dict[str, str] = {}
for entry in _WOL_DEVICES_RAW.split(","):
entry = entry.strip()
if not entry:
continue
parts = entry.split(":")
if len(parts) >= 8:
name = parts[0]
mac = ":".join(parts[1:7])
ip = parts[7]
devices[name] = mac
device_ips[name] = ip
elif len(parts) == 3:
name, mac, ip = parts
devices[name] = mac
device_ips[name] = ip
return devices, device_ips
DEVICES, DEVICE_IPS = _parse_devices()
def _ping_host(ip: str) -> bool:
try:
subprocess.check_output(
["ping", "-c", "1", "-W", "1", ip],
stderr=subprocess.DEVNULL,
)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
@app.route("/wake/<device>", methods=["POST"])
def wake_device(device):
if device not in DEVICES:
return jsonify({"error": "Device not found"}), 404
mac_address = DEVICES[device]
try:
for _ in range(max(1, WOL_PACKET_COUNT)):
send_magic_packet(mac_address, ip_address=BROADCAST_IP)
app.logger.info(
f"Magic packet(s) sent to {device} ({mac_address}) via {BROADCAST_IP}"
)
return jsonify({"message": f"Wake-on-LAN packet sent to {device}"}), 200
except Exception as e:
app.logger.exception(f"Error sending magic packet to {device}")
return jsonify({"error": f"Failed to send wake packet: {str(e)}"}), 500
@app.route("/devices", methods=["GET"])
def list_devices():
return jsonify({"devices": list(DEVICES.keys())})
@app.route("/status", methods=["GET"])
def device_status():
"""Return devices with MAC, IP, and online status."""
status = {
name: {
"mac": mac,
"ip": DEVICE_IPS.get(name, ""),
"online": _ping_host(DEVICE_IPS.get(name, "")),
}
for name, mac in DEVICES.items()
}
return jsonify({"devices": status})
@app.route("/debug/<device>", methods=["GET"])
def debug_device(device):
if device not in DEVICES:
return jsonify({"error": "Device not found"}), 404
return jsonify({
"device": device,
"mac_address": DEVICES[device],
"ip": DEVICE_IPS.get(device, ""),
"broadcast_ip": BROADCAST_IP,
"online": _ping_host(DEVICE_IPS.get(device, "")),
})
@app.route("/health", methods=["GET"])
def health_check():
return jsonify({"status": "healthy"}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=False)
+17
View File
@@ -0,0 +1,17 @@
services:
wol:
build: .
container_name: wol
restart: always
network_mode: host
environment:
- WOL_DEVICES=${WOL_DEVICES}
- WOL_BROADCAST_IP=${WOL_BROADCAST_IP:-192.168.0.255}
- WOL_PACKET_COUNT=${WOL_PACKET_COUNT:-3}
labels:
- "traefik.enable=true"
- "traefik.http.routers.wol.rule=Host(`wol.zea.lt`)"
- "traefik.http.routers.wol.entrypoints=websecure"
- "traefik.http.routers.wol.tls.certresolver=leresolver"
- "traefik.http.routers.wol.middlewares=auth@file"
- "traefik.http.services.wol.loadbalancer.server.port=5000"