added nginx to traefik migration changes and docs

This commit is contained in:
rezayaghobi-dev
2026-08-27 12:07:31 +03:30
parent 6f1431ad30
commit 00836e5b7c
36 changed files with 1229 additions and 74 deletions

View File

@@ -34,7 +34,8 @@ This is a real, running homelab — not a demo. It's deployed on a home Ubuntu s
| **Sentinel** | `docker-compose/sentinel/` | Disk health monitoring integrated with smartctl, dumps output to container logs | | **Sentinel** | `docker-compose/sentinel/` | Disk health monitoring integrated with smartctl, dumps output to container logs |
| **smartctl** | `docker-compose/smartctl/` | Disk health monitoring (S.M.A.R.T. data) | | **smartctl** | `docker-compose/smartctl/` | Disk health monitoring (S.M.A.R.T. data) |
| **OpenHands** | `docker-compose/openhands/` | Self-hosted AI coding agent (OpenHands/agent-canvas) backed by Ollama — see [AI Coding Agent](docs/AI-Agent.md) | | **OpenHands** | `docker-compose/openhands/` | Self-hosted AI coding agent (OpenHands/agent-canvas) backed by Ollama — see [AI Coding Agent](docs/AI-Agent.md) |
| **Nginx Proxy Manager** | `docker-compose/nginx-proxy-manager/` | Reverse proxy giving every service a friendly `.home` hostname with locally-trusted HTTPS — see [Reverse Proxy](docs/Reverse-Proxy.md) | | **Traefik** | `docker-compose/traefik/` | Reverse proxy with Docker daemon auto-discovery and full config-as-code — replaces Nginx Proxy Manager — see [Traefik](docs/Traefik.md) |
| **Vaultwarden** | `docker-compose/vaultwarden/` | Self-hosted Bitwarden-compatible password manager — see [Vaultwarden](docs/Vaultwarden.md) |
| **Semaphore** | `docker-compose/semaphore/` | Self-hosted Ansible UI (alternative to AWX) — see [Semaphore Guide](docs/Semaphore.md) | | **Semaphore** | `docker-compose/semaphore/` | Self-hosted Ansible UI (alternative to AWX) — see [Semaphore Guide](docs/Semaphore.md) |
| **Ansible playbooks** | `ansible/update-playbooks/` | Shared update role (pull → recreate → health-check → prune) reused across every stack, run via Semaphore — see [Ansible Playbooks](docs/AnsiblePlaybooks.md) | | **Ansible playbooks** | `ansible/update-playbooks/` | Shared update role (pull → recreate → health-check → prune) reused across every stack, run via Semaphore — see [Ansible Playbooks](docs/AnsiblePlaybooks.md) |
| **Server provisioning** | `ansible/preparing-playbook/` | Full server preparation playbook (packages, hardening, Docker, Nexus/Traefik) tested against Vagrant first — see [Ansible README](ansible/README.md) | | **Server provisioning** | `ansible/preparing-playbook/` | Full server preparation playbook (packages, hardening, Docker, Nexus/Traefik) tested against Vagrant first — see [Ansible README](ansible/README.md) |
@@ -50,6 +51,7 @@ Not everything in this repo runs permanently. Some folders are kept purely as re
| Lab | Folder | Purpose | | Lab | Folder | Purpose |
|---|---|---| |---|---|---|
| **Nginx Proxy Manager** | `docker-compose/nginx-proxy-manager/` | ~~Retired~~ — replaced by Traefik for config-as-code reverse proxying. See [Traefik](docs/Traefik.md) for the migration rationale and [Reverse Proxy](docs/Reverse-Proxy.md) for the original NPM setup |
| **Elastic Stack (ELK)** | `docker-compose/ELK/` | Elasticsearch + Kibana, security-enabled, sized for this hardware — a modern-workflow (Elastic Agent/Fleet) alternative to an older Filebeat/Metricbeat-based course reference. Not run continuously since Loki now covers centralized logging here — see [Elastic Stack (ELK)](docs/ElasticStack.md) | | **Elastic Stack (ELK)** | `docker-compose/ELK/` | Elasticsearch + Kibana, security-enabled, sized for this hardware — a modern-workflow (Elastic Agent/Fleet) alternative to an older Filebeat/Metricbeat-based course reference. Not run continuously since Loki now covers centralized logging here — see [Elastic Stack (ELK)](docs/ElasticStack.md) |
| **Vagrant test VM** | `vagrant/` | Disposable Debian 12 VirtualBox VM (`bento/debian-12`) on a private-network IP, for testing provisioning scripts and configuration changes before touching the real host | | **Vagrant test VM** | `vagrant/` | Disposable Debian 12 VirtualBox VM (`bento/debian-12`) on a private-network IP, for testing provisioning scripts and configuration changes before touching the real host |
@@ -100,8 +102,24 @@ homelab/
│ │ └── docker-compose.yml │ │ └── docker-compose.yml
│ ├── openhands/ │ ├── openhands/
│ │ └── docker-compose.yaml │ │ └── docker-compose.yaml
│ ├── traefik/
│ │ ├── compose.yaml
│ │ ├── README.md
│ │ ├── certs/
│ │ │ ├── home.server.crt
│ │ │ └── home.server.key
│ │ └── dynamic/
│ │ ├── tls.yaml
│ │ ├── middlewares.yaml
│ │ ├── pihole.yaml
│ │ ├── plex.yml
│ │ ├── sentinel.yml
│ │ └── dockscope-auth.yml
│ ├── vaultwarden/
│ │ ├── docker-compose.yml
│ │ └── vw-data/
│ ├── nginx-proxy-manager/ │ ├── nginx-proxy-manager/
│ │ └── docker-compose.yaml │ │ └── compose.yaml
│ ├── semaphore/ │ ├── semaphore/
│ │ ├── docker-compose.yml │ │ ├── docker-compose.yml
│ │ └── README.md │ │ └── README.md
@@ -216,9 +234,9 @@ Rather than pulling and recreating manually, every always-on stack has a matchin
## Security notes ## Security notes
- All `.env` files, runtime config directories, and databases are excluded via `.gitignore` — only deployment definitions (`docker-compose.yml`) and `.env.example` placeholders are tracked. - All `.env` files, runtime config directories, and databases are excluded via `.gitignore` — only deployment definitions (`docker-compose.yml`) and `.env.example` placeholders are tracked.
- Passwords referenced in compose files are injected via environment variables, never hardcoded. - Passwords referenced in compose files are injected via environment variables, never hardcoded. Long passwords (30+ chars) are generated and stored in Vaultwarden, referenced via `.env` files on the host.
- The AI coding agent (`docker-compose/openhands/`) currently uses a third-party API aggregator as its LLM backend — see [AI Coding Agent](docs/AI-Agent.md) for details and caveats. It has no access to the Docker socket or other containers on the host. - The AI coding agent (`docker-compose/openhands/`) currently uses a third-party API aggregator as its LLM backend — see [AI Coding Agent](docs/AI-Agent.md) for details and caveats. It has no access to the Docker socket or other containers on the host.
- **DockScope** (`docker-compose/dockscope/`) mounts `/var/run/docker.sock`, the same as Portainer — this grants it effectively root-equivalent access to the Docker host, and it currently has no built-in authentication on its web UI, so it's kept LAN-only and not exposed through the reverse proxy. See [DockScope](docs/DockScope.md). - **DockScope** (`docker-compose/dockscope/`) mounts `/var/run/docker.sock`, the same as Portainer — this grants it effectively root-equivalent access to the Docker host. It's protected by a Traefik basicAuth middleware and kept LAN-only. See [DockScope](docs/DockScope.md).
- **Grafana Alloy** (part of the monitoring stack) also mounts `/var/run/docker.sock` read-only, needed to discover containers and tail their logs. - **Grafana Alloy** (part of the monitoring stack) also mounts `/var/run/docker.sock` read-only, needed to discover containers and tail their logs.
## Lessons learned ## Lessons learned
@@ -244,6 +262,7 @@ Rather than pulling and recreating manually, every always-on stack has a matchin
- `kube-state-metrics`'s default ClusterRole doesn't cover every resource it tries to watch (`NetworkPolicy`, `MutatingWebhookConfiguration`, `ValidatingWebhookConfiguration`, and more) — surfaces as repeated `reflector.go` `Failed to list ... is forbidden` warnings rather than a single clear error. Fixed with a supplementary `ClusterRole`/`ClusterRoleBinding` granting just the missing `list`/`watch` verbs. See [K3s](docs/K3s.md). - `kube-state-metrics`'s default ClusterRole doesn't cover every resource it tries to watch (`NetworkPolicy`, `MutatingWebhookConfiguration`, `ValidatingWebhookConfiguration`, and more) — surfaces as repeated `reflector.go` `Failed to list ... is forbidden` warnings rather than a single clear error. Fixed with a supplementary `ClusterRole`/`ClusterRoleBinding` granting just the missing `list`/`watch` verbs. See [K3s](docs/K3s.md).
- A pod doesn't pick up a widened RBAC grant on its own — its informers already failed and cached that state. `kubectl rollout restart` is required after any `ClusterRole` change for an already-running pod to actually benefit from it. - A pod doesn't pick up a widened RBAC grant on its own — its informers already failed and cached that state. `kubectl rollout restart` is required after any `ClusterRole` change for an already-running pod to actually benefit from it.
- Old log lines don't disappear from Grafana/Loki after a fix ships — they just age out of whatever time window the dashboard is querying. A dropping "Errors total" over a few minutes is the real signal that a fix worked, not an empty panel immediately after applying it. - Old log lines don't disappear from Grafana/Loki after a fix ships — they just age out of whatever time window the dashboard is querying. A dropping "Errors total" over a few minutes is the real signal that a fix worked, not an empty panel immediately after applying it.
- Migrating from a UI-based reverse proxy (Nginx Proxy Manager) to a config-as-code one (Traefik) forced a cleanup of every hardcoded password in every compose file — the migration was the catalyst for adopting Vaultwarden and proper secret management across the stack. See [Traefik](docs/Traefik.md) and [Vaultwarden](docs/Vaultwarden.md).
## Roadmap ## Roadmap

View File

@@ -1,9 +1,48 @@
# ELK + Fleet — sized for: Phenom II X4, 8GB DDR3, 120GB SSD
#
# Scope right now = what the course has actually covered: Elasticsearch,
# Logstash, Kibana, Fleet Server. Filebeat/auditbeat/metricbeat/packetbeat/
# apm-server are deliberately NOT in here yet — add them as their own
# service blocks (same pattern as elasticsearch/kibana below) when you
# reach that lesson, so you're not paying RAM for services you can't use
# yet.
#
# Services are grouped with Compose "profiles" so you can turn whole
# lesson-chunks on/off without editing this file — just edit
# COMPOSE_PROFILES in .env. Core (elasticsearch + kibana) has no profile,
# so it's always included.
#
# Rough memory budget on this box (no swap):
# elasticsearch ~1.5 GB
# logstash ~0.9 GB
# kibana ~0.9 GB
# fleet-server ~0.4 GB
# ---------------------------------
# total ~3.7 GB (you have ~5.9 GB free right now)
#
# That leaves headroom for Docker/Traefik/host overhead and for the first
# enrolled Elastic Agent once you get there (~0.25-0.3 GB more).
#
# REGISTRY: docker.elastic.co sits behind Amazon CloudFront, which
# geo-blocks a handful of sanctioned countries (confirmed pattern for
# Russia/Belarus, same CloudFront-based mechanism affects Iran) — that's
# what "context deadline exceeded" against docker.elastic.co/v2/ almost
# always means, not a real network fault. These images pull instead from
# Docker Hub (elasticsearch, kibana, logstash under the official
# library/ namespace, elastic-agent under elastic/) — Elastic pushes the
# exact same images there and keeps them current (checked: 8.19.19
# present on all four as of this writing). Different CDN, so it isn't
# guaranteed to be reachable either — test with
# `docker pull elasticsearch:${ELK_VERSION}` before assuming it works.
services: services:
elasticsearch: elasticsearch:
image: elasticsearch:8.19.19 image: elasticsearch:${ELK_VERSION}
container_name: elasticsearch container_name: elasticsearch
restart: unless-stopped
environment: environment:
- node.name=elasticsearch - node.name=elasticsearch
- cluster.name=elk-lab
- discovery.type=single-node - discovery.type=single-node
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD} - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
- xpack.security.enabled=true - xpack.security.enabled=true
@@ -13,19 +52,37 @@ services:
- ES_JAVA_OPTS=-Xms1g -Xmx1g - ES_JAVA_OPTS=-Xms1g -Xmx1g
mem_limit: 1500m mem_limit: 1500m
cpus: 1.5 cpus: 1.5
ulimits:
memlock:
soft: -1
hard: -1
volumes: volumes:
- es-data:/usr/share/elasticsearch/data - es-data:/usr/share/elasticsearch/data
ports: ports:
- "9200:9200" - "9200:9200" # keep for now, drop once you trust Traefik routing
restart: unless-stopped networks:
- elk
- web_net
healthcheck: healthcheck:
test: ["CMD-SHELL", "curl -s -u elastic:${ELASTIC_PASSWORD} http://localhost:9200 | grep -q cluster_name"] test: ["CMD-SHELL", "curl -s -u elastic:${ELASTIC_PASSWORD} http://localhost:9200 | grep -q cluster_name"]
interval: 10s interval: 10s
timeout: 10s timeout: 10s
retries: 30 retries: 30
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
- "traefik.http.routers.elasticsearch.rule=Host(`elasticsearch.home.server`)"
- "traefik.http.routers.elasticsearch.entrypoints=websecure"
- "traefik.http.routers.elasticsearch.tls=true"
- "traefik.http.routers.elasticsearch.service=elasticsearch"
- "traefik.http.routers.elasticsearch.middlewares=security-headers@file"
- "traefik.http.routers.elasticsearch-http.rule=Host(`elasticsearch.home.server`)"
- "traefik.http.routers.elasticsearch-http.entrypoints=web"
- "traefik.http.routers.elasticsearch-http.middlewares=redirect-to-https@file"
- "traefik.http.services.elasticsearch.loadbalancer.server.port=9200"
kibana-setup: kibana-setup:
image: elasticsearch:8.19.19 image: elasticsearch:${ELK_VERSION}
container_name: kibana-setup container_name: kibana-setup
depends_on: depends_on:
elasticsearch: elasticsearch:
@@ -43,13 +100,16 @@ services:
- "Content-Type: application/json" - "Content-Type: application/json"
- "-d" - "-d"
- "{\"password\":\"${KIBANA_PASSWORD}\"}" - "{\"password\":\"${KIBANA_PASSWORD}\"}"
networks:
- elk
kibana: kibana:
image: kibana:8.19.19 image: kibana:${ELK_VERSION}
container_name: kibana container_name: kibana
depends_on: depends_on:
kibana-setup: kibana-setup:
condition: service_completed_successfully condition: service_completed_successfully
restart: unless-stopped
environment: environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200 - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
- ELASTICSEARCH_USERNAME=kibana_system - ELASTICSEARCH_USERNAME=kibana_system
@@ -57,12 +117,125 @@ services:
- XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY=${KIBANA_ENCRYPTION_KEY} - XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY=${KIBANA_ENCRYPTION_KEY}
- XPACK_SECURITY_ENCRYPTIONKEY=${KIBANA_ENCRYPTION_KEY} - XPACK_SECURITY_ENCRYPTIONKEY=${KIBANA_ENCRYPTION_KEY}
- XPACK_REPORTING_ENCRYPTIONKEY=${KIBANA_ENCRYPTION_KEY} - XPACK_REPORTING_ENCRYPTIONKEY=${KIBANA_ENCRYPTION_KEY}
- NODE_OPTIONS=--max-old-space-size=1024 - XPACK_FLEET_ENABLED=true
mem_limit: 1200m - SERVER_PUBLICBASEURL=https://kibana.home.server
- NODE_OPTIONS=--max-old-space-size=896
mem_limit: 900m
cpus: 1 cpus: 1
ports: ports:
- "5601:5601" - "5601:5601" # keep for now
networks:
- elk
- web_net
healthcheck:
test: ["CMD-SHELL", "curl -s http://localhost:5601/api/status | grep -q '\"level\":\"available\"'"]
interval: 10s
timeout: 10s
retries: 30
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
- "traefik.http.routers.kibana.rule=Host(`kibana.home.server`)"
- "traefik.http.routers.kibana.entrypoints=websecure"
- "traefik.http.routers.kibana.tls=true"
- "traefik.http.routers.kibana.service=kibana"
- "traefik.http.routers.kibana.middlewares=security-headers@file"
- "traefik.http.routers.kibana-http.rule=Host(`kibana.home.server`)"
- "traefik.http.routers.kibana-http.entrypoints=web"
- "traefik.http.routers.kibana-http.middlewares=redirect-to-https@file"
- "traefik.http.services.kibana.loadbalancer.server.port=5601"
logstash:
image: logstash:${ELK_VERSION}
container_name: logstash
profiles: ["logstash"]
depends_on:
elasticsearch:
condition: service_healthy
restart: unless-stopped restart: unless-stopped
environment:
- LS_JAVA_OPTS=-Xms512m -Xmx512m
- ELASTICSEARCH_USERNAME=elastic
- ELASTICSEARCH_PASSWORD=${ELASTIC_PASSWORD}
mem_limit: 900m
cpus: 1
volumes:
- ./logstash/pipeline:/usr/share/logstash/pipeline:ro
- ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml:ro
ports:
- "5044:5044" # beats input
- "9600:9600" # monitoring API
networks:
- elk
healthcheck:
test: ["CMD-SHELL", "curl -s http://localhost:9600 | grep -q ok"]
interval: 10s
timeout: 10s
retries: 30
fleet-server:
image: elastic/elastic-agent:${ELK_VERSION}
container_name: fleet-server
profiles: ["fleet"]
depends_on:
kibana:
condition: service_healthy
restart: unless-stopped
environment:
# No service token here on purpose — the agent bootstraps Fleet
# Server itself (creates the policy, mints its own token) as long
# as it can log into Kibana with real credentials. Without these
# two it silently falls back to elastic/changeme and fails auth.
- ELASTICSEARCH_USERNAME=elastic
- ELASTICSEARCH_PASSWORD=${ELASTIC_PASSWORD}
- FLEET_SERVER_ENABLE=true
- FLEET_SERVER_ELASTICSEARCH_HOST=http://elasticsearch:9200
- FLEET_SERVER_INSECURE_HTTP=true
- FLEET_SERVER_HOST=0.0.0.0
- FLEET_SERVER_PORT=8220
- KIBANA_HOST=http://kibana:5601
- KIBANA_FLEET_HOST=http://kibana:5601
mem_limit: 400m
cpus: 0.5
volumes:
- fleet-server-state:/usr/share/elastic-agent/state
ports:
- "8220:8220"
networks:
- elk
- web_net
healthcheck:
test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8220/api/status | grep -q HEALTHY"]
interval: 10s
timeout: 5s
retries: 30
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
- "traefik.http.routers.fleet.rule=Host(`fleet.home.server`)"
- "traefik.http.routers.fleet.entrypoints=websecure"
- "traefik.http.routers.fleet.tls=true"
- "traefik.http.routers.fleet.service=fleet"
- "traefik.http.routers.fleet.middlewares=security-headers@file"
- "traefik.http.routers.fleet-http.rule=Host(`fleet.home.server`)"
- "traefik.http.routers.fleet-http.entrypoints=web"
- "traefik.http.routers.fleet-http.middlewares=redirect-to-https@file"
- "traefik.http.services.fleet.loadbalancer.server.port=8220"
# --- Next lesson, not started yet ---
# Once you reach filebeat.yml / auditbeat.yml etc, add each beat here as
# its own service (profiles: ["beats"]), same pattern as logstash above:
# mount its .yml under ./<beat-name>/, point ELASTICSEARCH_HOSTNAME etc
# at elasticsearch/kibana over the `elk` network, and give it a named
# volume for its data dir. No Traefik labels needed for beats — they're
# agents, not web UIs.
volumes: volumes:
es-data: es-data:
fleet-server-state:
networks:
elk:
driver: bridge
web_net:
external: true

View File

@@ -0,0 +1,24 @@
input {
beats {
port => 5044
}
}
filter {
# Add your grok/mutate/etc filters here as you get to that lesson.
}
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
user => "elastic"
password => "${ELASTICSEARCH_PASSWORD}"
index => "logstash-%{+YYYY.MM.dd}"
}
# Comment this out once you trust the pipeline — it's just here so you
# can see events flowing while you're learning.
stdout {
codec => rubydebug
}
}

View File

@@ -1,18 +1,28 @@
services: services:
semaphore: dockscope:
image: semaphoreui/semaphore:latest image: ghcr.io/manuelr-t/dockscope:latest
container_name: semaphore container_name: dockscope
ports:
- "3001:3000"
environment:
SEMAPHORE_DB_DIALECT: sqlite
SEMAPHORE_DB_PATH: /var/lib/semaphore/database.db
SEMAPHORE_ADMIN: admin
SEMAPHORE_ADMIN_PASSWORD: rezayaghobi1386
SEMAPHORE_ADMIN_EMAIL: rezay5639@gmail.com
SEMAPHORE_ADMIN_NAME: Admin
SEMAPHORE_SECRET_KEY: cc4b3a5a90d31b963bee48a9dee089472bef5e0461f934db7891fc68161d36f7
volumes: volumes:
- ./data:/var/lib/semaphore - /var/run/docker.sock:/var/run/docker.sock
- ./playbooks:/playbooks
restart: unless-stopped restart: unless-stopped
networks:
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# HTTPS
- "traefik.http.routers.dockscope.rule=Host(`dockscope.home.server`)"
- "traefik.http.routers.dockscope.entrypoints=websecure"
- "traefik.http.routers.dockscope.tls=true"
- "traefik.http.routers.dockscope.service=dockscope"
- "traefik.http.routers.dockscope.middlewares=security-headers@file,dockscope-auth@file"
# HTTP → HTTPS redirect
- "traefik.http.routers.dockscope-http.rule=Host(`dockscope.home.server`)"
- "traefik.http.routers.dockscope-http.entrypoints=web"
- "traefik.http.routers.dockscope-http.middlewares=redirect-to-https@file"
# Internal port
- "traefik.http.services.dockscope.loadbalancer.server.port=4681"
networks:
web_net:
external: true

View File

@@ -2,18 +2,41 @@ services:
filebrowser: filebrowser:
image: filebrowser/filebrowser:latest image: filebrowser/filebrowser:latest
container_name: filebrowser container_name: filebrowser
ports: restart: unless-stopped
- "8085:80"
user: "1000:1000" user: "1000:1000"
ports:
- "8085:80" # keep for now (fallback)
volumes: volumes:
- /srv/samba/games:/games - /srv/samba/games:/games
- /mnt/storage:/storage - /mnt/storage:/storage
- /home/rezayaghobi/docker/filebrowser/config:/config - /home/rezayaghobi/docker/filebrowser/config:/config
- filebrowser_db:/database - filebrowser_db:/database
- filebrowser_srv:/srv - filebrowser_srv:/srv
restart: unless-stopped networks:
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# HTTPS
- "traefik.http.routers.filebrowser.rule=Host(`filebrowser.home.server`)"
- "traefik.http.routers.filebrowser.entrypoints=websecure"
- "traefik.http.routers.filebrowser.tls=true"
- "traefik.http.routers.filebrowser.service=filebrowser"
- "traefik.http.routers.filebrowser.middlewares=security-headers@file"
# HTTP → HTTPS redirect
- "traefik.http.routers.filebrowser-http.rule=Host(`filebrowser.home.server`)"
- "traefik.http.routers.filebrowser-http.entrypoints=web"
- "traefik.http.routers.filebrowser-http.middlewares=redirect-to-https@file"
# Internal port (Filebrowser listens on 80 inside the container)
- "traefik.http.services.filebrowser.loadbalancer.server.port=80"
volumes: volumes:
filebrowser_db: filebrowser_db:
filebrowser_srv: filebrowser_srv:
networks:
web_net:
external: true

View File

@@ -5,9 +5,7 @@ services:
restart: unless-stopped restart: unless-stopped
hostname: gitlab.local hostname: gitlab.local
ports: ports:
- "8929:80"
- "2224:22" - "2224:22"
volumes: volumes:
- ./config:/etc/gitlab - ./config:/etc/gitlab
- ./logs:/var/log/gitlab - ./logs:/var/log/gitlab
@@ -16,10 +14,14 @@ services:
environment: environment:
GITLAB_OMNIBUS_CONFIG: | GITLAB_OMNIBUS_CONFIG: |
gitlab_rails['monitoring_whitelist'] = ['127.0.0.1', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'] gitlab_rails['monitoring_whitelist'] = ['127.0.0.1', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
external_url 'http://192.168.100.6:8929' external_url 'https://gitlab.home.server'
nginx['listen_port'] = 80 nginx['listen_port'] = 80
nginx['listen_https'] = false nginx['listen_https'] = false
nginx['proxy_set_headers'] = {
"X-Forwarded-Proto" => "https",
"X-Forwarded-Ssl" => "on"
}
gitlab_rails['gitlab_shell_ssh_port'] = 2224 gitlab_rails['gitlab_shell_ssh_port'] = 2224
@@ -38,14 +40,52 @@ services:
postgres_exporter['enable'] = false postgres_exporter['enable'] = false
gitlab_exporter['enable'] = false gitlab_exporter['enable'] = false
# Registry off for now — flip to true later if you want it # Registry
registry['enable'] = false registry['enable'] = true
registry_external_url 'https://registry.home.server'
gitlab_rails['registry_enabled'] = true
gitlab_rails['registry_host'] = "registry.home.server"
gitlab_rails['registry_port'] = "443"
registry_nginx['listen_port'] = 5050
registry_nginx['listen_https'] = false
networks:
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# --- Web UI ---
- "traefik.http.routers.gitlab.rule=Host(`gitlab.home.server`)"
- "traefik.http.routers.gitlab.entrypoints=websecure"
- "traefik.http.routers.gitlab.tls=true"
- "traefik.http.routers.gitlab.service=gitlab"
- "traefik.http.routers.gitlab.middlewares=security-headers@file"
- "traefik.http.routers.gitlab-http.rule=Host(`gitlab.home.server`)"
- "traefik.http.routers.gitlab-http.entrypoints=web"
- "traefik.http.routers.gitlab-http.service=gitlab"
- "traefik.http.routers.gitlab-http.middlewares=redirect-to-https@file"
- "traefik.http.services.gitlab.loadbalancer.server.port=80"
# --- Container Registry ---
- "traefik.http.routers.registry.rule=Host(`registry.home.server`)"
- "traefik.http.routers.registry.entrypoints=websecure"
- "traefik.http.routers.registry.tls=true"
- "traefik.http.routers.registry.service=registry"
- "traefik.http.routers.registry.middlewares=security-headers@file"
- "traefik.http.routers.registry-http.rule=Host(`registry.home.server`)"
- "traefik.http.routers.registry-http.entrypoints=web"
- "traefik.http.routers.registry-http.service=registry"
- "traefik.http.routers.registry-http.middlewares=redirect-to-https@file"
- "traefik.http.services.registry.loadbalancer.server.port=5050"
gitlab-runner: gitlab-runner:
image: gitlab/gitlab-runner:latest image: gitlab/gitlab-runner:latest
container_name: gitlab-runner container_name: gitlab-runner
restart: unless-stopped restart: unless-stopped
ports:
- "9252:9252"
volumes: volumes:
- ./runner-config:/etc/gitlab-runner - ./runner-config:/etc/gitlab-runner
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
networks:
web_net:
external: true

View File

@@ -17,7 +17,25 @@ services:
- /mnt/storage/films/downloads:/downloads - /mnt/storage/films/downloads:/downloads
networks: networks:
- mediastack - mediastack
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# HTTPS
- "traefik.http.routers.qbittorrent.rule=Host(`qbittorrent.home.server`)"
- "traefik.http.routers.qbittorrent.entrypoints=websecure"
- "traefik.http.routers.qbittorrent.tls=true"
- "traefik.http.routers.qbittorrent.service=qbittorrent"
- "traefik.http.routers.qbittorrent.middlewares=security-headers@file"
# HTTP → HTTPS redirect
- "traefik.http.routers.qbittorrent-http.rule=Host(`qbittorrent.home.server`)"
- "traefik.http.routers.qbittorrent-http.entrypoints=web"
- "traefik.http.routers.qbittorrent-http.middlewares=redirect-to-https@file"
# Internal port (qbittorrent WebUI listens on 8080 inside the container)
- "traefik.http.services.qbittorrent.loadbalancer.server.port=8080"
networks: networks:
mediastack: mediastack:
external: true external: true
web_net:
external: true

View File

@@ -2,11 +2,12 @@ services:
plex: plex:
image: lscr.io/linuxserver/plex:latest image: lscr.io/linuxserver/plex:latest
container_name: plex container_name: plex
restart: unless-stopped
network_mode: host network_mode: host
environment: environment:
- TZ=Asia/Tehran - TZ=Asia/Tehran
- PLEX_CLAIM=claim-uRK14oDz86FsyFgNyaBh
volumes: volumes:
- /home/rezayaghobi/docker/plex:/config - /home/rezayaghobi/docker/plex:/config
- /mnt/storage:/media - /mnt/storage:/media
restart: unless-stopped

View File

@@ -13,7 +13,28 @@ services:
- ./config:/config - ./config:/config
networks: networks:
- mediastack - mediastack
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# HTTPS
- "traefik.http.routers.prowlarr.rule=Host(`prowlarr.home.server`)"
- "traefik.http.routers.prowlarr.entrypoints=websecure"
- "traefik.http.routers.prowlarr.tls=true"
- "traefik.http.routers.prowlarr.service=prowlarr"
- "traefik.http.routers.prowlarr.middlewares=security-headers@file"
# HTTP → HTTPS redirect
- "traefik.http.routers.prowlarr-http.rule=Host(`prowlarr.home.server`)"
- "traefik.http.routers.prowlarr-http.entrypoints=web"
- "traefik.http.routers.prowlarr-http.middlewares=redirect-to-https@file"
# Internal port (prowlarr listens on 9696 inside the container)
- "traefik.http.services.prowlarr.loadbalancer.server.port=9696"
networks: networks:
mediastack: mediastack:
external: true external: true
web_net:
external: true

View File

@@ -10,13 +10,30 @@ services:
- ./config:/config - ./config:/config
- /mnt/storage/films:/media - /mnt/storage/films:/media
- /mnt/storage/films/downloads:/downloads - /mnt/storage/films/downloads:/downloads
ports: ports:
- 7878:7878 - 7878:7878
restart: unless-stopped restart: unless-stopped
networks: networks:
- mediastack - mediastack
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# HTTPS
- "traefik.http.routers.radarr.rule=Host(`radarr.home.server`)"
- "traefik.http.routers.radarr.entrypoints=websecure"
- "traefik.http.routers.radarr.tls=true"
- "traefik.http.routers.radarr.service=radarr"
- "traefik.http.routers.radarr.middlewares=security-headers@file"
# HTTP → HTTPS redirect
- "traefik.http.routers.radarr-http.rule=Host(`radarr.home.server`)"
- "traefik.http.routers.radarr-http.entrypoints=web"
- "traefik.http.routers.radarr-http.middlewares=redirect-to-https@file"
# Internal port (radarr listens on 7878 inside the container)
- "traefik.http.services.radarr.loadbalancer.server.port=7878"
networks: networks:
mediastack: mediastack:
external: true external: true
web_net:
external: true

View File

@@ -16,7 +16,29 @@ services:
restart: unless-stopped restart: unless-stopped
networks: networks:
- mediastack - mediastack
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# HTTPS
- "traefik.http.routers.sonarr.rule=Host(`sonarr.home.server`)"
- "traefik.http.routers.sonarr.entrypoints=websecure"
- "traefik.http.routers.sonarr.tls=true"
- "traefik.http.routers.sonarr.service=sonarr"
- "traefik.http.routers.sonarr.middlewares=security-headers@file"
# HTTP → HTTPS redirect
- "traefik.http.routers.sonarr-http.rule=Host(`sonarr.home.server`)"
- "traefik.http.routers.sonarr-http.entrypoints=web"
- "traefik.http.routers.sonarr-http.middlewares=redirect-to-https@file"
# Internal port (Filebrowser listens on 8989 inside the container)
- "traefik.http.services.sonarr.loadbalancer.server.port=8989"
networks: networks:
mediastack: mediastack:
external: true external: true
web_net:
external: true

View File

@@ -15,10 +15,27 @@ services:
- /mnt/storage/films/downloads:/downloads - /mnt/storage/films/downloads:/downloads
- /mnt/storage/adult:/data/whisparr - /mnt/storage/adult:/data/whisparr
- /mnt/storage/films/downloads/whisparr:/downloads/whisparr # ADD THIS - /mnt/storage/films/downloads/whisparr:/downloads/whisparr # ADD THIS
networks: networks:
- mediastack - mediastack
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# HTTPS
- "traefik.http.routers.whisparr.rule=Host(`whisparr.home.server`)"
- "traefik.http.routers.whisparr.entrypoints=websecure"
- "traefik.http.routers.whisparr.tls=true"
- "traefik.http.routers.whisparr.service=whisparr"
- "traefik.http.routers.whisparr.middlewares=security-headers@file"
# HTTP → HTTPS redirect
- "traefik.http.routers.whisparr-http.rule=Host(`whisparr.home.server`)"
- "traefik.http.routers.whisparr-http.entrypoints=web"
- "traefik.http.routers.whisparr-http.middlewares=redirect-to-https@file"
# Internal port (whisparr listens on 6969 inside the container)
- "traefik.http.services.whisparr.loadbalancer.server.port=6969"
networks: networks:
mediastack: mediastack:
external: true external: true
web_net:
external: true

View File

@@ -3,13 +3,54 @@ services:
image: minio/minio:RELEASE.2024-01-18T22-51-28Z-cpuv1 image: minio/minio:RELEASE.2024-01-18T22-51-28Z-cpuv1
container_name: minio container_name: minio
restart: unless-stopped restart: unless-stopped
ports:
- "9500:9000"
- "9501:9001"
environment: environment:
MINIO_ROOT_USER: admin MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: change this MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
MINIO_BROWSER_REDIRECT_URL: https://minio.home.server
volumes: volumes:
- ./data:/data - ./data:/data
command: server /data --console-address ":9001" command: server /data --console-address ":9001"
mem_limit: 256m mem_limit: 256m
networks:
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# Console
- "traefik.http.routers.minio-console.rule=Host(`minio.home.server`)"
- "traefik.http.routers.minio-console.entrypoints=websecure"
- "traefik.http.routers.minio-console.tls=true"
- "traefik.http.routers.minio-console.service=minio-console"
- "traefik.http.routers.minio-console.middlewares=security-headers@file"
- "traefik.http.services.minio-console.loadbalancer.server.port=9001"
# HTTP -> HTTPS
- "traefik.http.routers.minio-console-http.rule=Host(`minio.home.server`)"
- "traefik.http.routers.minio-console-http.entrypoints=web"
- "traefik.http.routers.minio-console-http.middlewares=redirect-to-https@file"
- "traefik.http.routers.minio-console-http.service=minio-console"
# S3 API
- "traefik.http.routers.minio-api.rule=Host(`s3.home.server`)"
- "traefik.http.routers.minio-api.entrypoints=websecure"
- "traefik.http.routers.minio-api.tls=true"
- "traefik.http.routers.minio-api.service=minio-api"
- "traefik.http.routers.minio-api.middlewares=security-headers@file"
- "traefik.http.services.minio-api.loadbalancer.server.port=9000"
# HTTP -> HTTPS
- "traefik.http.routers.minio-api-http.rule=Host(`s3.home.server`)"
- "traefik.http.routers.minio-api-http.entrypoints=web"
- "traefik.http.routers.minio-api-http.middlewares=redirect-to-https@file"
- "traefik.http.routers.minio-api-http.service=minio-api"
networks:
web_net:
external: true

View File

@@ -4,7 +4,7 @@ services:
container_name: prometheus container_name: prometheus
restart: unless-stopped restart: unless-stopped
ports: ports:
- "9090:9090" - "9090:9090" # keep for now
volumes: volumes:
- ./prometheus:/etc/prometheus:ro - ./prometheus:/etc/prometheus:ro
- ./data/prometheus:/prometheus - ./data/prometheus:/prometheus
@@ -14,18 +14,44 @@ services:
- "--web.enable-lifecycle" - "--web.enable-lifecycle"
networks: networks:
- monitoring - monitoring
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
- "traefik.http.routers.prometheus.rule=Host(`prometheus.home.server`)"
- "traefik.http.routers.prometheus.entrypoints=websecure"
- "traefik.http.routers.prometheus.tls=true"
- "traefik.http.routers.prometheus.service=prometheus"
- "traefik.http.routers.prometheus.middlewares=security-headers@file"
- "traefik.http.routers.prometheus-http.rule=Host(`prometheus.home.server`)"
- "traefik.http.routers.prometheus-http.entrypoints=web"
- "traefik.http.routers.prometheus-http.middlewares=redirect-to-https@file"
- "traefik.http.services.prometheus.loadbalancer.server.port=9090"
grafana: grafana:
image: grafana/grafana:latest image: grafana/grafana:latest
container_name: grafana container_name: grafana
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3000:3000" - "3000:3000" # keep for now
volumes: volumes:
- ./grafana/data:/var/lib/grafana - ./grafana/data:/var/lib/grafana
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
networks: networks:
- monitoring - monitoring
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
- "traefik.http.routers.grafana.rule=Host(`grafana.home.server`)"
- "traefik.http.routers.grafana.entrypoints=websecure"
- "traefik.http.routers.grafana.tls=true"
- "traefik.http.routers.grafana.service=grafana"
- "traefik.http.routers.grafana.middlewares=security-headers@file"
- "traefik.http.routers.grafana-http.rule=Host(`grafana.home.server`)"
- "traefik.http.routers.grafana-http.entrypoints=web"
- "traefik.http.routers.grafana-http.middlewares=redirect-to-https@file"
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
node_exporter: node_exporter:
image: prom/node-exporter:latest image: prom/node-exporter:latest
@@ -39,13 +65,14 @@ services:
- /:/rootfs:ro - /:/rootfs:ro
networks: networks:
- monitoring - monitoring
# no Traefik labels – no web UI needed
cadvisor: cadvisor:
image: ghcr.io/google/cadvisor:latest image: ghcr.io/google/cadvisor:latest
container_name: cadvisor container_name: cadvisor
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8080:8080" - "8080:8080" # keep for now
volumes: volumes:
- /:/rootfs:ro - /:/rootfs:ro
- /var/run:/var/run:rw - /var/run:/var/run:rw
@@ -53,6 +80,19 @@ services:
- /var/lib/docker/:/var/lib/docker:ro - /var/lib/docker/:/var/lib/docker:ro
networks: networks:
- monitoring - monitoring
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
- "traefik.http.routers.cadvisor.rule=Host(`cadvisor.home.server`)"
- "traefik.http.routers.cadvisor.entrypoints=websecure"
- "traefik.http.routers.cadvisor.tls=true"
- "traefik.http.routers.cadvisor.service=cadvisor"
- "traefik.http.routers.cadvisor.middlewares=security-headers@file"
- "traefik.http.routers.cadvisor-http.rule=Host(`cadvisor.home.server`)"
- "traefik.http.routers.cadvisor-http.entrypoints=web"
- "traefik.http.routers.cadvisor-http.middlewares=redirect-to-https@file"
- "traefik.http.services.cadvisor.loadbalancer.server.port=8080"
loki: loki:
image: grafana/loki:3.7.3 image: grafana/loki:3.7.3
@@ -62,12 +102,25 @@ services:
ports: ports:
- "3100:3100" - "3100:3100"
volumes: volumes:
- ./loki/local-config.yaml:/etc/loki/local-config.yaml:ro - ./loki/local-config.yaml:/etc/loki/local-config.yaml:ro
- ./data/loki:/loki - ./data/loki:/loki
mem_limit: 400m mem_limit: 400m
cpus: 1 cpus: 1
networks: networks:
- monitoring - monitoring
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
- "traefik.http.routers.loki.rule=Host(`loki.home.server`)"
- "traefik.http.routers.loki.entrypoints=websecure"
- "traefik.http.routers.loki.tls=true"
- "traefik.http.routers.loki.service=loki"
- "traefik.http.routers.loki.middlewares=security-headers@file"
- "traefik.http.routers.loki-http.rule=Host(`loki.home.server`)"
- "traefik.http.routers.loki-http.entrypoints=web"
- "traefik.http.routers.loki-http.middlewares=redirect-to-https@file"
- "traefik.http.services.loki.loadbalancer.server.port=3100"
alloy: alloy:
image: grafana/alloy:v1.17.1 image: grafana/alloy:v1.17.1
@@ -91,7 +144,22 @@ services:
- loki - loki
networks: networks:
- monitoring - monitoring
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
- "traefik.http.routers.alloy.rule=Host(`alloy.home.server`)"
- "traefik.http.routers.alloy.entrypoints=websecure"
- "traefik.http.routers.alloy.tls=true"
- "traefik.http.routers.alloy.service=alloy"
- "traefik.http.routers.alloy.middlewares=security-headers@file"
- "traefik.http.routers.alloy-http.rule=Host(`alloy.home.server`)"
- "traefik.http.routers.alloy-http.entrypoints=web"
- "traefik.http.routers.alloy-http.middlewares=redirect-to-https@file"
- "traefik.http.services.alloy.loadbalancer.server.port=12345"
networks: networks:
monitoring: monitoring:
driver: bridge driver: bridge
web_net:
external: true

View File

@@ -36,23 +36,54 @@ services:
DB_POSTGRESDB_USER: ${POSTGRES_USER} DB_POSTGRESDB_USER: ${POSTGRES_USER}
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD} DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
NPM_ADMIN_EMAIL: ${NPM_ADMIN_EMAIL}
NPM_ADMIN_PASSWORD: ${NPM_ADMIN_PASSWORD}
PIHOLE_PASSWORD: ${PIHOLE_PASSWORD}
N8N_BASIC_AUTH_ACTIVE: true N8N_BASIC_AUTH_ACTIVE: true
N8N_BLOCK_ENV_ACCESS_IN_NODE: false
N8N_BASIC_AUTH_USER: ${N8N_BASIC_AUTH_USER} N8N_BASIC_AUTH_USER: ${N8N_BASIC_AUTH_USER}
N8N_BASIC_AUTH_PASSWORD: ${N8N_BASIC_AUTH_PASSWORD} N8N_BASIC_AUTH_PASSWORD: ${N8N_BASIC_AUTH_PASSWORD}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY} N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
N8N_SECURE_COOKIE: false N8N_SECURE_COOKIE: false
N8N_HOST: ${N8N_HOST} N8N_HOST: n8n.home.server
N8N_PROTOCOL: https
N8N_PORT: 5678 N8N_PORT: 5678
N8N_PROTOCOL: http WEBHOOK_URL: https://n8n.home.server/
TZ: ${TZ} TZ: ${TZ}
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# HTTPS router
- "traefik.http.routers.n8n.rule=Host(`n8n.home.server`)"
- "traefik.http.routers.n8n.entrypoints=websecure"
- "traefik.http.routers.n8n.tls=true"
- "traefik.http.routers.n8n.service=n8n"
- "traefik.http.routers.n8n.middlewares=security-headers@file"
# HTTP → HTTPS redirect
- "traefik.http.routers.n8n-http.rule=Host(`n8n.home.server`)"
- "traefik.http.routers.n8n-http.entrypoints=web"
- "traefik.http.routers.n8n-http.middlewares=redirect-to-https@file"
# Internal container port
- "traefik.http.services.n8n.loadbalancer.server.port=5678"
ports: ports:
- "5678:5678" - "5678:5678"
networks:
- default # keep the existing n8n_default (for postgres)
- web_net # new
volumes: volumes:
- ./n8n-data:/home/node/.n8n - ./n8n-data:/home/node/.n8n
- /mnt/storage:/mnt/storage - /mnt/storage:/mnt/storage
- /mnt/windows-video:/mnt/windows-video - /mnt/windows-video:/mnt/windows-video
networks:
web_net:
external: true

View File

@@ -10,14 +10,19 @@ services:
- ./projects:/home/openhands/workspace/project - ./projects:/home/openhands/workspace/project
stdin_open: true stdin_open: true
tty: true tty: true
ollama: depends_on:
image: ollama/ollama:latest - github-mcp
container_name: ollama
github-mcp:
image: ghcr.io/github/github-mcp-server:latest
container_name: github-mcp
restart: unless-stopped restart: unless-stopped
ports: ports:
- "11434:11434" - "8083:8083"
volumes: stdin_open: true
- ollama-data:/root/.ollama tty: true
environment:
- GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN}
volumes: volumes:
openhands-data: openhands-data:
ollama-data:

View File

@@ -1 +1 @@
PIHOLE_WEBPASSWORD=change-me PIHOLE_WEBPASSWORD=CHANGE_ME

View File

@@ -3,11 +3,10 @@ services:
container_name: pihole container_name: pihole
image: ghcr.io/pi-hole/pihole:latest image: ghcr.io/pi-hole/pihole:latest
network_mode: host network_mode: host
env_file:
- .env
environment: environment:
TZ: "Asia/Tehran" TZ: "Asia/Tehran"
WEBPASSWORD: ${PIHOLE_WEBPASSWORD} WEBPASSWORD: ${PIHOLE_WEBPASSWORD}
FTLCONF_webserver_port: '8082o,[::]:8082o'
volumes: volumes:
- ./etc-pihole:/etc/pihole - ./etc-pihole:/etc/pihole
- ./etc-dnsmasq.d:/etc/dnsmasq.d - ./etc-dnsmasq.d:/etc/dnsmasq.d

View File

@@ -4,7 +4,31 @@ services:
container_name: portainer container_name: portainer
restart: unless-stopped restart: unless-stopped
ports: ports:
- "9000:9000" - "9001:9000" # keep for now (fallback)
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- /home/rezayaghobi/docker/portainer/data:/data - /home/rezayaghobi/docker/portainer/data:/data
networks:
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# HTTPS
- "traefik.http.routers.portainer.rule=Host(`portainer.home.server`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls=true"
- "traefik.http.routers.portainer.service=portainer"
- "traefik.http.routers.portainer.middlewares=security-headers@file"
# HTTP → HTTPS redirect
- "traefik.http.routers.portainer-http.rule=Host(`portainer.home.server`)"
- "traefik.http.routers.portainer-http.entrypoints=web"
- "traefik.http.routers.portainer-http.middlewares=redirect-to-https@file"
# Internal port (Portainer listens on 9000 inside the container)
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
networks:
web_net:
external: true

View File

@@ -0,0 +1,3 @@
SEMAPHORE_ADMIN_PASSWORD=CHANGE_ME
SEMAPHORE_ADMIN_EMAIL=your-email@example.com
SEMAPHORE_SECRET_KEY=CHANGE_ME_generate_with_openssl_rand_hex_32

View File

@@ -8,10 +8,32 @@ services:
SEMAPHORE_DB_DIALECT: sqlite SEMAPHORE_DB_DIALECT: sqlite
SEMAPHORE_DB_PATH: /var/lib/semaphore/database.db SEMAPHORE_DB_PATH: /var/lib/semaphore/database.db
SEMAPHORE_ADMIN: admin SEMAPHORE_ADMIN: admin
SEMAPHORE_ADMIN_PASSWORD: choose a passwor or use .env (better ) SEMAPHORE_ADMIN_PASSWORD: ${SEMAPHORE_ADMIN_PASSWORD}
SEMAPHORE_ADMIN_EMAIL: rezay5639@gmail.com SEMAPHORE_ADMIN_EMAIL: ${SEMAPHORE_ADMIN_EMAIL}
SEMAPHORE_ADMIN_NAME: Admin SEMAPHORE_ADMIN_NAME: Admin
SEMAPHORE_SECRET_KEY: <your-long-secret-key> SEMAPHORE_SECRET_KEY: ${SEMAPHORE_SECRET_KEY}
volumes: volumes:
- ./data:/var/lib/semaphore - ./data:/var/lib/semaphore
- ./playbooks:/playbooks
networks:
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# HTTPS
- "traefik.http.routers.semaphore.rule=Host(`semaphore.home.server`)"
- "traefik.http.routers.semaphore.entrypoints=websecure"
- "traefik.http.routers.semaphore.tls=true"
- "traefik.http.routers.semaphore.service=semaphore"
- "traefik.http.routers.semaphore.middlewares=security-headers@file"
# HTTP → HTTPS redirect
- "traefik.http.routers.semaphore-http.rule=Host(`semaphore.home.server`)"
- "traefik.http.routers.semaphore-http.entrypoints=web"
- "traefik.http.routers.semaphore-http.middlewares=redirect-to-https@file"
# Internal port (semaphore listens on 3000 inside the container)
- "traefik.http.services.semaphore.loadbalancer.server.port=3000"
restart: unless-stopped restart: unless-stopped
networks:
web_net:
external: true

View File

@@ -6,7 +6,5 @@ services:
volumes: volumes:
- /dev:/dev - /dev:/dev
- /run/udev:/run/udev:ro - /run/udev:/run/udev:ro
ports: - ./report:/etc/hdsentinel
- "8090:8090" restart: unless-stopped
restart: unless-stopped

View File

@@ -0,0 +1,134 @@
# Traefik
Self-hosted reverse proxy replacing Nginx Proxy Manager — gives every service a friendly `<service>.home.server` hostname with locally-trusted HTTPS, configured entirely as code.
## Why the migration from Nginx Proxy Manager
NPM works fine, but the switching cost grew as the homelab did. Two things pushed the move:
### 1. Infrastructure as code
NPM's entire configuration lives behind a web UI — proxy hosts, SSL certificates, custom locations, access rules. None of it is version-controlled, exportable, or reproducible. If the container dies and comes back without its data volume, you're rebuilding every proxy host by hand.
Traefik is the opposite: every route, middleware, TLS cert, and entrypoint is defined in YAML files that live in this repo. The compose file, the `dynamic/` configs, the cert files — all version-controlled, all diffable, all reproducible with `docker compose up -d`. This is the same pattern the rest of the homelab already follows (each service is a Compose file in a folder), and Traefik fits that model naturally where NPM didn't.
### 2. Docker daemon auto-discovery
NPM requires you to manually add each proxy host through its web UI — type the domain, point it at the container's IP and port, attach an SSL cert, save. Every new service means another round-trip through the UI.
Traefik talks directly to the Docker daemon and watches for containers with `traefik.enable=true`. Add a few labels to any compose file and Traefik picks up the new route automatically — no UI interaction, no restart, no manual configuration. This means adding a new service to the reverse proxy is a two-line addition to that service's compose file, not a separate step in a different tool.
## How it works
```
traefik/
├── compose.yaml # Traefik container + static config
├── certs/ # Local TLS certificates (mkcert)
│ ├── home.server.crt
│ └── home.server.key
└── dynamic/ # File-based provider — routes, middlewares, TLS
├── tls.yaml # Default TLS cert store
├── middlewares.yaml # HTTPS redirect + security headers
├── pihole.yaml # Pi-hole route
├── plex.yml # Plex route (external host)
├── sentinel.yml # Sentinel route (external host)
└── dockscope-auth.yml # Basic auth middleware for DockScope
```
### Static config (`compose.yaml`)
Traefik's static configuration is passed as command-line flags:
- **Docker provider** — watches the Docker daemon for labeled containers on the `web_net` network
- **File provider** — watches `/dynamic/` for YAML config changes (hot-reloaded, no restart needed)
- **Entrypoints** — `:80` (HTTP) and `:443` (HTTPS)
- **API dashboard** — enabled but not insecure (accessed via its own HTTPS route)
### Dynamic config (`dynamic/`)
Routes and middlewares are defined as YAML files, not labels. This keeps the compose files clean and makes it easy to see all routing rules in one place:
- **`middlewares.yaml`** — shared `redirect-to-https` and `security-headers` middlewares (HSTS, XSS filter, content-type nosniff, SAMEORIGIN framing)
- **`tls.yaml`** — default TLS cert store pointing at the mkcert-generated certificate
- **`pihole.yaml`**, **`plex.yml`**, **`sentinel.yml`** — per-service route definitions for services that can't use Docker labels (host-network or external hosts)
### Services using Docker labels
Most services define their Traefik routes directly in their compose files via labels:
```yaml
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
- "traefik.http.routers.<service>.rule=Host(`<service>.home.server`)"
- "traefik.http.routers.<service>.entrypoints=websecure"
- "traefik.http.routers.<service>.tls=true"
- "traefik.http.routers.<service>.middlewares=security-headers@file"
- "traefik.http.routers.<service>-http.rule=Host(`<service>.home.server`)"
- "traefik.http.routers.<service>-http.entrypoints=web"
- "traefik.http.routers.<service>-http.middlewares=redirect-to-https@file"
- "traefik.http.services.<service>.loadbalancer.server.port=<internal-port>"
```
Services currently fronted by Traefik:
| Service | Hostname | Internal Port |
|---|---|---|
| Prometheus | `prometheus.home.server` | 9090 |
| Grafana | `grafana.home.server` | 3000 |
| cAdvisor | `cadvisor.home.server` | 8080 |
| Loki | `loki.home.server` | 3100 |
| Alloy | `alloy.home.server` | 12345 |
| Pi-hole | `pihole.home.server` | 8082 (host) |
| Portainer | `portainer.home.server` | 9000 |
| n8n | `n8n.home.server` | 5678 |
| Filebrowser | `filebrowser.home.server` | 80 |
| DockScope | `dockscope.home.server` | 4681 |
| Semaphore | `semaphore.home.server` | 3000 |
| MinIO | `minio.home.server` | 9001 |
| Plex | `plex.home.server` | 32400 (external) |
| Sentinel | `sentinel.home.server` | 8088 (external) |
| Vaultwarden | `vaultwarden.home.server` | 80 |
| Traefik Dashboard | `traefik.home.server` | API@internal |
## TLS certificates
Same approach as before — [mkcert](https://github.com/FiloSottile/mkcert) generates a local CA and signs certificates for `*.home.server`. The cert and key live in `certs/` and are mounted read-only into the Traefik container.
Traefik loads the default cert via `dynamic/tls.yaml`:
```yaml
tls:
stores:
default:
defaultCertificate:
certFile: /certs/home.server.crt
keyFile: /certs/home.server.key
```
Adding a new service with a `.home.server` hostname automatically gets the right cert — no per-host certificate assignment needed.
## Adding a new service
1. **If the service is a Docker container on `web_net`** — add Traefik labels to its compose file (copy the pattern from any existing service above)
2. **If the service is host-network or external** — create a new YAML file in `dynamic/` with the router and service definition
3. **Add the hostname to Pi-hole** — so DNS resolves `<service>.home.server` to the server's LAN IP
No restart required — Traefik watches both the Docker daemon and the `dynamic/` directory and picks up changes automatically.
## Network
All Traefik-fronted services join the `web_net` external Docker network. This network must be created before starting Traefik:
```bash
docker network create web_net
```
## Security notes
- The Traefik dashboard is behind its own HTTPS route with security headers — not exposed on the public internet, LAN-only via Pi-hole DNS
- DockScope additionally uses a basicAuth middleware (`dynamic/dockscope-auth.yml`) since it mounts the Docker socket
- All HTTP requests are redirected to HTTPS permanently (301)
- Security headers include HSTS (1 year, includeSubdomains, preload), XSS filter, content-type nosniff, and SAMEORIGIN framing
[← Back to Home](../../docs/Home.md)

View File

@@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEQTCCAqmgAwIBAgIQMHgxq9We6lR+AyMJeJHo7DANBgkqhkiG9w0BAQsFADB3
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExJjAkBgNVBAsMHXJlemF5
YWdob2JpQGhvbWVzZXJ2ZXIgKHJlemEpMS0wKwYDVQQDDCRta2NlcnQgcmV6YXlh
Z2hvYmlAaG9tZXNlcnZlciAocmV6YSkwHhcNMjYwODI0MDgzODU5WhcNMjgxMTI0
MDgzODU5WjBRMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNh
dGUxJjAkBgNVBAsMHXJlemF5YWdob2JpQGhvbWVzZXJ2ZXIgKHJlemEpMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvgzUWThVUpVqdqkoJ8325vsC5Eoe
mNlihoZ+zXenBBpZgvuiMdz+duBGPdWQcXK7DQr0jBXNqx0wt8w4RSg9pBETPNXT
mcRxudzzvKggVVzUqJ6MrODj2NSNCxUL05oR/iH30+pmliCRE3CyjEvlnpcUVdu1
Qwp0LdxC4fR2ZNbWQbRZ3aoQXYwaQ7WaFno9tlyhvhBdh/rtQOMhi6kVdtB8pTGY
Zzc1fSBJsOCqlwyN6jcp/J8jZ3AfCU1fdqZwMXuQd0TbUOv/K1grQ99IkzJu2zKJ
jKomBdD1SZnn+/buUl2KGAl1e/G6/LIDgoKVDjb7y4XNCcZX4oNx7pxPdwIDAQAB
o28wbTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0j
BBgwFoAUuWL4Syj7LA309BRwMJjRz3eC+FUwJQYDVR0RBB4wHIINKi5ob21lLnNl
cnZlcoILaG9tZS5zZXJ2ZXIwDQYJKoZIhvcNAQELBQADggGBAKmBR+udJASNvgtN
LtQzHcq790hnnVg/O1dbSd0Ah2PSrz/OtYOVMgOTa9WPIYeMjVKVOsRcPlR8lO7O
udJuDUi3QkSQ+hmsqFKqC9Mt9uPg6ozcGNcX8/jC/eTIJKYlQCdI69brehR/R4rd
xQo+1MYv/JUULUy/1Ix6uGs80jNuVGwwEnfTxU0K47FUI7hx5HjR8wHv0cMY3Fb9
tUEJJfWjyuyq+oXB3x8loEEBL7ESQUb0pCKKmtmclerm0ia1X5Sh2Sn+d/Me3RHC
ABh0zwQcmC0j/r4T5ajTr5ZK5jzo+3NTtqpLF0TiKAPioGr+W9sJlw5IZhlIVpN8
UqNrwvUbyiVuNUrl70QabO0G3PFUX/Kfi1tXh+9eletFpz3CBQrrJljOFoXhzVFt
FZGPiQuKkAryDvdMpS5oRnOcgR/hROIkz6ACkECwvvbWE5wb7BX+9NZm39gpNe8r
5Cj1OyF6+HZ+o5NHOKzcj4Ci0/e0m+I4Fh9kG5ZXC5+cX79z/A==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,56 @@
services:
traefik:
image: traefik:v3.7
container_name: traefik
restart: unless-stopped
command:
- --log.level=INFO
# API / dashboard
- --api.dashboard=true
- --api.insecure=false
# Docker provider
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=web_net
- --providers.file.directory=/dynamic
- --providers.file.watch=true
# Entrypoints
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
# Health check
- --ping=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./dynamic:/dynamic:ro
- ./certs:/certs:ro
networks:
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# Dashboard HTTPS
- "traefik.http.routers.traefik.rule=Host(`traefik.home.server`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls=true"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.middlewares=security-headers@file"
# Dashboard HTTP → HTTPS redirect
- "traefik.http.routers.traefik-http.rule=Host(`traefik.home.server`)"
- "traefik.http.routers.traefik-http.entrypoints=web"
- "traefik.http.routers.traefik-http.middlewares=redirect-to-https@file"
networks:
web_net:
external: true

View File

@@ -0,0 +1,7 @@
http:
middlewares:
dockscope-auth:
basicAuth:
users:
# Generate with: htpasswd -nB <username>
- "${DOCKSCOPE_AUTH_USER}:${DOCKSCOPE_AUTH_HASH}"

View File

@@ -0,0 +1,17 @@
http:
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
permanent: true
# Optional but recommended security headers (we can tune later)
security-headers:
headers:
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
customFrameOptionsValue: "SAMEORIGIN"

View File

@@ -0,0 +1,24 @@
http:
routers:
pihole:
rule: "Host(`pihole.home.server`)"
entryPoints:
- websecure
tls: {}
service: pihole
middlewares:
- security-headers@file
pihole-http:
rule: "Host(`pihole.home.server`)"
entryPoints:
- web
service: pihole # ← added (required)
middlewares:
- redirect-to-https@file
services:
pihole:
loadBalancer:
servers:
- url: "http://172.17.0.1:8082"

View File

@@ -0,0 +1,24 @@
http:
routers:
plex:
rule: "Host(`plex.home.server`)"
entryPoints:
- websecure
tls: {}
service: plex
middlewares:
- security-headers@file
plex-http:
rule: "Host(`plex.home.server`)"
entryPoints:
- web
service: plex
middlewares:
- redirect-to-https@file
services:
plex:
loadBalancer:
servers:
- url: "http://192.168.100.6:32400"

View File

@@ -0,0 +1,24 @@
http:
routers:
sentinel:
rule: "Host(`sentinel.home.server`)"
entryPoints:
- websecure
tls: true
service: sentinel
middlewares:
- security-headers@file
sentinel-http:
rule: "Host(`sentinel.home.server`)"
entryPoints:
- web
service: sentinel
middlewares:
- redirect-to-https@file
services:
sentinel:
loadBalancer:
servers:
- url: "http://192.168.100.6:8088"

View File

@@ -0,0 +1,6 @@
tls:
stores:
default:
defaultCertificate:
certFile: /certs/home.server.crt
keyFile: /certs/home.server.key

View File

@@ -0,0 +1,33 @@
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
- WEBSOCKET_ENABLED=true
- SIGNUPS_ALLOWED=false # set false after creating your account
volumes:
- ./vw-data:/data
ports:
- "8084:80"
networks:
- web_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=web_net"
# HTTPS
- "traefik.http.routers.vaultwarden.rule=Host(`vaultwarden.home.server`)"
- "traefik.http.routers.vaultwarden.entrypoints=websecure"
- "traefik.http.routers.vaultwarden.tls=true"
- "traefik.http.routers.vaultwarden.service=vaultwarden"
- "traefik.http.routers.vaultwarden.middlewares=security-headers@file"
# HTTP → HTTPS redirect
- "traefik.http.routers.vaultwarden-http.rule=Host(`vaultwarden.home.server`)"
- "traefik.http.routers.vaultwarden-http.entrypoints=web"
- "traefik.http.routers.vaultwarden-http.middlewares=redirect-to-https@file"
# Internal port (vaultwarden listens on 80 inside the container)
- "traefik.http.services.vaultwarden.loadbalancer.server.port=80"
networks:
web_net:
external: true

View File

@@ -13,7 +13,9 @@ Documentation for a self-hosted homelab running entirely on Docker Compose. This
- [Storage Health](Storage-Health.md) - [Storage Health](Storage-Health.md)
- [Lessons Learned](Lessons-Learned.md) - [Lessons Learned](Lessons-Learned.md)
- [AI Coding Agent](AI-Agent.md) - [AI Coding Agent](AI-Agent.md)
- [Reverse Proxy](Reverse-Proxy.md) - [Reverse Proxy](Reverse-Proxy.md) (historical — Nginx Proxy Manager)
- [Traefik](Traefik.md) (current reverse proxy)
- [Vaultwarden](Vaultwarden.md) (password manager)
- [Semaphore](Semaphore.md) - [Semaphore](Semaphore.md)
- [DockScope](DockScope.md) - [DockScope](DockScope.md)
- [Ansible Playbooks](AnsiblePlaybooks.md) - [Ansible Playbooks](AnsiblePlaybooks.md)
@@ -30,7 +32,7 @@ Documentation for a self-hosted homelab running entirely on Docker Compose. This
| **Orchestration** | Docker Compose, one stack per service | | **Orchestration** | Docker Compose, one stack per service |
| **Management** | Portainer, **DockScope** (3D visual dashboard) | | **Management** | Portainer, **DockScope** (3D visual dashboard) |
| **Monitoring** | Prometheus + Grafana + cAdvisor + node-exporter + **Loki + Alloy** (centralized logs) | | **Monitoring** | Prometheus + Grafana + cAdvisor + node-exporter + **Loki + Alloy** (centralized logs) |
| **Network** | Pi-hole (DNS filtering), Samba (file sharing), nginx (proxy managing) | | **Network** | Pi-hole (DNS filtering), Samba (file sharing), **Traefik** (reverse proxy, config-as-code) |
| **Automation** | n8n + **Semaphore** (Ansible Web UI) + **Ansible** (update playbooks) || **Media** | Plex + Sonarr/Radarr/Prowlarr + qBittorrent | | **Automation** | n8n + **Semaphore** (Ansible Web UI) + **Ansible** (update playbooks) || **Media** | Plex + Sonarr/Radarr/Prowlarr + qBittorrent |
| **Disk health** | Hard Disk Sentinel | | **Disk health** | Hard Disk Sentinel |
| **AI Agent** | OpenHands + Ollama | | **AI Agent** | OpenHands + Ollama |

View File

@@ -1,6 +1,8 @@
# Reverse Proxy # Reverse Proxy (historical — Nginx Proxy Manager)
[Nginx Proxy Manager](https://nginxproxymanager.com/) sits in front of every service on the LAN, replacing `http://<ip>:<port>` with friendly `<service>.home` names and trusted local HTTPS. Runs as its own Compose stack under `nginx-proxy-manager/`. > **Note:** This homelab has migrated from Nginx Proxy Manager to **Traefik**. See [Traefik](Traefik.md) for the current reverse proxy setup and migration rationale. This page documents the original NPM setup as a reference.
[Nginx Proxy Manager](https://nginxproxymanager.com/) sat in front of every service on the LAN, replacing `http://<ip>:<port>` with friendly `<service>.home` names and trusted local HTTPS. Ran as its own Compose stack under `nginx-proxy-manager/`.
![Sanctions/access error](images/openhands-error.png) ![Sanctions/access error](images/openhands-error.png)

129
docs/Traefik.md Normal file
View File

@@ -0,0 +1,129 @@
# Traefik — Reverse Proxy
Traefik replaced Nginx Proxy Manager as the homelab's reverse proxy, giving every service a `<service>.home.server` hostname with locally-trusted HTTPS — now configured entirely as code instead of a web UI.
See the [Traefik README](../docker-compose/traefik/README.md) for the full file-by-file breakdown and how to add new services.
## Why the migration
Two reasons, both rooted in the same observation: NPM works, but it doesn't scale with the rest of the infrastructure-as-code approach.
**Everything as code.** NPM's configuration lives entirely in its web UI — proxy hosts, SSL certs, custom locations, access rules. None of it is version-controlled or reproducible. If the data volume is lost, you're rebuilding every proxy host manually. Traefik's routes, middlewares, and TLS config are all YAML files in this repo — diffable, reviewable, reproducible with `docker compose up -d`.
**Docker daemon auto-discovery.** NPM requires manually adding each proxy host through its UI — domain, target IP/port, SSL cert, save. Every new service means another round-trip through a different tool. Traefik watches the Docker daemon directly and picks up containers with `traefik.enable=true` automatically. Adding a new service to the reverse proxy is a two-line label addition to that service's compose file.
## Architecture
```
┌─────────────────────────────────────────────────────┐
│ Traefik v3.7 │
│ │
│ Entrypoints: :80 (HTTP) → redirect to HTTPS │
│ :443 (HTTPS) → route to service │
│ │
│ Providers: │
│ Docker daemon → auto-discovers labeled conns │
│ File provider → watches dynamic/ for YAML │
│ │
│ TLS: mkcert local CA, single cert for all hosts │
└─────────────────────────────────────────────────────┘
│ │ │
┌────┴────┐ ┌─────┴─────┐ ┌────┴────┐
│ Docker │ │ File │ │ certs/ │
│ labels │ │ dynamic/ │ │ mkcert │
│ per-svc │ │ routes │ │ CA │
└─────────┘ └───────────┘ └─────────┘
```
### Static config
Passed as command-line flags in `compose.yaml`:
- **Docker provider** — watches for labeled containers on `web_net`
- **File provider** — hot-reloads `dynamic/` directory
- **Entrypoints** — `:80` and `:443`
- **Ping** — health check endpoint
- **API dashboard** — enabled, secured via its own HTTPS route
### Dynamic config (`dynamic/`)
| File | Purpose |
|---|---|
| `tls.yaml` | Default TLS cert store pointing at the mkcert certificate |
| `middlewares.yaml` | `redirect-to-https` (301) + `security-headers` (HSTS, XSS, nosniff, SAMEORIGIN) |
| `pihole.yaml` | Pi-hole route (host-network, can't use Docker labels) |
| `plex.yml` | Plex route (external host `192.168.100.6:32400`) |
| `sentinel.yml` | Sentinel route (external host `192.168.100.6:8088`) |
| `dockscope-auth.yml` | BasicAuth middleware for DockScope (Docker socket access) |
### Services fronted by Traefik
| Service | Hostname | Method |
|---|---|---|
| Prometheus | `prometheus.home.server` | Docker labels |
| Grafana | `grafana.home.server` | Docker labels |
| cAdvisor | `cadvisor.home.server` | Docker labels |
| Loki | `loki.home.server` | Docker labels |
| Alloy | `alloy.home.server` | Docker labels |
| Pi-hole | `pihole.home.server` | File provider |
| Portainer | `portainer.home.server` | Docker labels |
| n8n | `n8n.home.server` | Docker labels |
| Filebrowser | `filebrowser.home.server` | Docker labels |
| DockScope | `dockscope.home.server` | Docker labels + basicAuth |
| Semaphore | `semaphore.home.server` | Docker labels |
| MinIO | `minio.home.server` | Docker labels |
| Plex | `plex.home.server` | File provider (external) |
| Sentinel | `sentinel.home.server` | File provider (external) |
| Vaultwarden | `vaultwarden.home.server` | Docker labels |
| Traefik Dashboard | `traefik.home.server` | Docker labels |
## TLS certificates
Same approach as the NPM era — [mkcert](https://github.com/FiloSottile/mkcert) creates a local CA trusted on each device, then signs a single certificate covering all `*.home.server` hostnames. The cert and key live in `traefik/certs/` and are mounted read-only.
Traefik's file-based TLS store (`dynamic/tls.yaml`) applies this cert as the default for all routes — no per-host certificate assignment needed. Adding a new `.home.server` hostname automatically gets the right cert.
## Network
All Traefik-fronted services join the `web_net` external Docker network:
```bash
docker network create web_net
```
Services that can't join `web_net` (host-network containers like Pi-hole, or external machines like Plex) are routed via the file provider with explicit IP:port targets.
## Adding a new service
1. **Docker container on `web_net`** — add Traefik labels to its compose file
2. **Host-network or external** — create a YAML file in `dynamic/`
3. **Pi-hole** — add DNS record for `<service>.home.server`
No restart required — Traefik auto-discovers Docker label changes and hot-reloads file provider changes.
## Port 80/443 conflict with Pi-hole
Pi-hole runs in `network_mode: host`, which means it binds directly to ports 80 and 443 for its web UI — the same ports Traefik needs. Resolved by moving Pi-hole's web interface to port 8082 via the FTL environment variable:
```yaml
environment:
FTLCONF_webserver_port: '8082o,[::]:8082o'
```
Pi-hole's DNS service (port 53) is untouched — only its web UI moved. See [Reverse Proxy](Reverse-Proxy.md) for the full history of this port conflict.
## Security notes
- Dashboard is LAN-only via Pi-hole DNS (not exposed publicly)
- DockScope has basicAuth in addition to HTTPS (Docker socket access)
- All HTTP → HTTPS redirects are permanent (301)
- Security headers: HSTS (1 year, includeSubdomains, preload), XSS filter, content-type nosniff, SAMEORIGIN framing
## Lessons learned
- Traefik's Docker label approach means the reverse proxy configuration is co-located with each service — no separate tool or UI to maintain. This matches the "each service is a folder" philosophy the rest of the homelab already follows.
- File-based routes in `dynamic/` are useful for services that can't use Docker labels (host-network containers, external machines). Keeping them as separate YAML files makes it obvious which services use which routing method.
- The `web_net` external network is a hard requirement — Traefik can only route to containers on networks it's connected to. Forgetting to add a new service to `web_net` is the most common "why isn't my route working" issue.
- Hot-reloading (file provider + Docker provider) means zero-downtime config changes — a major operational improvement over NPM, which required a container restart for some config changes.
[← Back to Home](Home.md)

96
docs/Vaultwarden.md Normal file
View File

@@ -0,0 +1,96 @@
# Vaultwarden
Self-hosted password manager — a lightweight, Rust-based implementation of the Bitwarden server API, giving full Bitwarden client compatibility (browser extensions, mobile apps, desktop apps) without Bitwarden's cloud dependency.
See [docker-compose/vaultwarden/](../docker-compose/vaultwarden/) for the deployment files.
## Why
After years of managing passwords across various tools, a self-hosted password manager was the missing piece. Vaultwarden provides:
- **Full Bitwarden compatibility** — works with the official Bitwarden browser extension, iOS/Android apps, and desktop apps. No proprietary client lock-in.
- **Lightweight** — single container, SQLite database, minimal resource usage. Runs comfortably alongside the rest of the stack.
- **Local data ownership** — passwords never leave the LAN unless you want them to (via Bitwarden's sync protocol for mobile access outside the home).
- **Long passwords** — using Vaultwarden as the source of truth for 30+ character passwords across all services, replacing the short memorable passwords that were previously hardcoded in compose files.
## Setup
### Initial configuration
```bash
cd docker-compose/vaultwarden
docker compose up -d
```
Access the web UI at `vaultwarden.home.server` (via Traefik) or `http://localhost:8084` directly.
**First step:** create your admin account, then set `SIGNUPS_ALLOWED=false` in the compose file to prevent new registrations.
### Generating password hashes
Vaultwarden uses the same `admin_token` mechanism as Bitwarden for admin access. For the Traefik basicAuth middleware (DockScope), generate hashes with:
```bash
htpasswd -nB <username>
```
This produces bcrypt-hashed entries compatible with Traefik's basicAuth middleware.
## Integration with the homelab
### Traefik routing
Vaultwarden is fronted by Traefik with the standard label set:
- `vaultwarden.home.server` → HTTPS → container port 80
- HTTP → HTTPS redirect
- Security headers middleware
### Password management workflow
1. Store all service passwords in Vaultwarden (30+ character randomly generated)
2. Reference passwords via `.env` files on the host (not in compose files)
3. Compose files use `${VARIABLE}` references, never hardcoded values
This is the migration path from the previous approach of short, memorable passwords hardcoded in compose files — the Traefik migration was the catalyst for adopting proper secret management across the stack.
## Configuration
Key environment variables in `docker-compose.yml`:
| Variable | Value | Purpose |
|---|---|---|
| `WEBSOCKET_ENABLED` | `true` | Enable real-time sync with Bitwarden clients |
| `SIGNUPS_ALLOWED` | `false` | Disable new account creation after initial setup |
Data is stored in `./vw-data/` (gitignored).
## Security notes
- Signups are disabled after initial account creation
- The web UI is only accessible via HTTPS through Traefik (LAN-only via Pi-hole DNS)
- Vaultwarden encrypts all vault data client-side before transmission — even if the server is compromised, passwords remain encrypted
- The `vw-data/` directory contains the SQLite database and should be backed up regularly
- Admin panel is accessible at `vaultwarden.home.server/admin` (requires admin token)
## Backups
The SQLite database in `vw-data/` should be backed up as part of the regular backup strategy. A simple approach:
```bash
# stop the container, copy the data, restart
docker compose stop vaultwarden
cp -r vw-data/ /backups/vaultwarden-$(date +%Y%m%d)/
docker compose start vaultwarden
```
Or use the Bitwarden CLI to export a JSON/CSV backup from within the web UI.
## Lessons learned
- Migrating from hardcoded passwords to a password manager is painful but worth it — the Traefik migration forced the cleanup, and now every service uses proper secret references.
- 30+ character passwords are free with a password manager — there's no reason to use short, memorable passwords for services that only need them in `.env` files.
- Vaultwarden's Bitwarden compatibility means you don't have to convince family members to use a different app — the official Bitwarden clients just work.
- The `SIGNUPS_ALLOWED` flag is easy to forget — leave it `true` too long and anyone on the LAN can create an account.
[← Back to Home](Home.md)