From 00836e5b7c766f313a6d0a360407736fc8dc2197 Mon Sep 17 00:00:00 2001 From: rezayaghobi-dev Date: Thu, 27 Aug 2026 12:07:31 +0330 Subject: [PATCH] added nginx to traefik migration changes and docs --- README.md | 27 ++- docker-compose/ELK/docker-compose.yml | 189 +++++++++++++++++- .../ELK/logstash/pipeline/logstash.conf | 24 +++ docker-compose/dockscope/docker-compose.yml | 40 ++-- docker-compose/filebrowser/docker-compose.yml | 29 ++- docker-compose/gitlab/docker-compose.yml | 54 ++++- .../docker-compose.yml | 18 ++ .../mediastack/plex/docker-compose.yml | 5 +- .../mediastack/prowlarr/docker-compose.yml | 21 ++ .../mediastack/radarr/docker-compose.yml | 19 +- .../mediastack/sonarr/docker-compose.yml | 22 ++ .../mediastack/whisparr/docker-compose.yml | 19 +- docker-compose/minio/docker-compose.yml | 51 ++++- .../docker-compose.yml | 76 ++++++- docker-compose/n8n/docker-compose.yaml | 35 +++- .../{compose.yaml => docker-compose.yml} | 19 +- docker-compose/pihole/.env.example | 2 +- docker-compose/pihole/docker-compose.yml | 3 +- docker-compose/portainer/docker-compose.yml | 26 ++- docker-compose/semaphore/.env.example | 3 + docker-compose/semaphore/docker-compose.yml | 28 ++- docker-compose/sentinel/docker-compose.yml | 6 +- docker-compose/traefik/README.md | 134 +++++++++++++ docker-compose/traefik/certs/home.server.crt | 25 +++ docker-compose/traefik/compose.yaml | 56 ++++++ .../traefik/dynamic/dockscope-auth.yml | 7 + .../traefik/dynamic/middlewares.yaml | 17 ++ docker-compose/traefik/dynamic/pihole.yaml | 24 +++ docker-compose/traefik/dynamic/plex.yml | 24 +++ docker-compose/traefik/dynamic/sentinel.yml | 24 +++ docker-compose/traefik/dynamic/tls.yaml | 6 + docker-compose/vaultwarden/docker-compose.yml | 33 +++ docs/Home.md | 6 +- docs/Reverse-Proxy.md | 6 +- docs/Traefik.md | 129 ++++++++++++ docs/Vaultwarden.md | 96 +++++++++ 36 files changed, 1229 insertions(+), 74 deletions(-) create mode 100644 docker-compose/ELK/logstash/pipeline/logstash.conf rename docker-compose/openhands/{compose.yaml => docker-compose.yml} (58%) create mode 100644 docker-compose/semaphore/.env.example create mode 100644 docker-compose/traefik/README.md create mode 100644 docker-compose/traefik/certs/home.server.crt create mode 100644 docker-compose/traefik/compose.yaml create mode 100644 docker-compose/traefik/dynamic/dockscope-auth.yml create mode 100644 docker-compose/traefik/dynamic/middlewares.yaml create mode 100644 docker-compose/traefik/dynamic/pihole.yaml create mode 100644 docker-compose/traefik/dynamic/plex.yml create mode 100644 docker-compose/traefik/dynamic/sentinel.yml create mode 100644 docker-compose/traefik/dynamic/tls.yaml create mode 100644 docker-compose/vaultwarden/docker-compose.yml create mode 100644 docs/Traefik.md create mode 100644 docs/Vaultwarden.md diff --git a/README.md b/README.md index 4f50c9b..6b0cf80 100644 --- a/README.md +++ b/README.md @@ -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 | | **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) | -| **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) | | **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) | @@ -50,6 +51,7 @@ Not everything in this repo runs permanently. Some folders are kept purely as re | 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) | | **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 │ ├── openhands/ │ │ └── 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/ -│ │ └── docker-compose.yaml +│ │ └── compose.yaml │ ├── semaphore/ │ │ ├── docker-compose.yml │ │ └── README.md @@ -216,9 +234,9 @@ Rather than pulling and recreating manually, every always-on stack has a matchin ## 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. -- 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. -- **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. ## 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). - 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. +- 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 diff --git a/docker-compose/ELK/docker-compose.yml b/docker-compose/ELK/docker-compose.yml index 4013af3..09a69ba 100644 --- a/docker-compose/ELK/docker-compose.yml +++ b/docker-compose/ELK/docker-compose.yml @@ -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: elasticsearch: - image: elasticsearch:8.19.19 + image: elasticsearch:${ELK_VERSION} container_name: elasticsearch + restart: unless-stopped environment: - node.name=elasticsearch + - cluster.name=elk-lab - discovery.type=single-node - ELASTIC_PASSWORD=${ELASTIC_PASSWORD} - xpack.security.enabled=true @@ -13,19 +52,37 @@ services: - ES_JAVA_OPTS=-Xms1g -Xmx1g mem_limit: 1500m cpus: 1.5 + ulimits: + memlock: + soft: -1 + hard: -1 volumes: - es-data:/usr/share/elasticsearch/data ports: - - "9200:9200" - restart: unless-stopped + - "9200:9200" # keep for now, drop once you trust Traefik routing + networks: + - elk + - web_net healthcheck: test: ["CMD-SHELL", "curl -s -u elastic:${ELASTIC_PASSWORD} http://localhost:9200 | grep -q cluster_name"] interval: 10s timeout: 10s 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: - image: elasticsearch:8.19.19 + image: elasticsearch:${ELK_VERSION} container_name: kibana-setup depends_on: elasticsearch: @@ -43,13 +100,16 @@ services: - "Content-Type: application/json" - "-d" - "{\"password\":\"${KIBANA_PASSWORD}\"}" + networks: + - elk kibana: - image: kibana:8.19.19 + image: kibana:${ELK_VERSION} container_name: kibana depends_on: kibana-setup: condition: service_completed_successfully + restart: unless-stopped environment: - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 - ELASTICSEARCH_USERNAME=kibana_system @@ -57,12 +117,125 @@ services: - XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY=${KIBANA_ENCRYPTION_KEY} - XPACK_SECURITY_ENCRYPTIONKEY=${KIBANA_ENCRYPTION_KEY} - XPACK_REPORTING_ENCRYPTIONKEY=${KIBANA_ENCRYPTION_KEY} - - NODE_OPTIONS=--max-old-space-size=1024 - mem_limit: 1200m + - XPACK_FLEET_ENABLED=true + - SERVER_PUBLICBASEURL=https://kibana.home.server + - NODE_OPTIONS=--max-old-space-size=896 + mem_limit: 900m cpus: 1 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 + 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 .//, 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: es-data: + fleet-server-state: + +networks: + elk: + driver: bridge + web_net: + external: true diff --git a/docker-compose/ELK/logstash/pipeline/logstash.conf b/docker-compose/ELK/logstash/pipeline/logstash.conf new file mode 100644 index 0000000..a855095 --- /dev/null +++ b/docker-compose/ELK/logstash/pipeline/logstash.conf @@ -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 + } +} diff --git a/docker-compose/dockscope/docker-compose.yml b/docker-compose/dockscope/docker-compose.yml index cdb3b68..a4af414 100644 --- a/docker-compose/dockscope/docker-compose.yml +++ b/docker-compose/dockscope/docker-compose.yml @@ -1,18 +1,28 @@ services: - semaphore: - image: semaphoreui/semaphore:latest - container_name: semaphore - 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 + dockscope: + image: ghcr.io/manuelr-t/dockscope:latest + container_name: dockscope volumes: - - ./data:/var/lib/semaphore - - ./playbooks:/playbooks + - /var/run/docker.sock:/var/run/docker.sock 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 \ No newline at end of file diff --git a/docker-compose/filebrowser/docker-compose.yml b/docker-compose/filebrowser/docker-compose.yml index 0dfd3bf..aa27a5b 100644 --- a/docker-compose/filebrowser/docker-compose.yml +++ b/docker-compose/filebrowser/docker-compose.yml @@ -2,18 +2,41 @@ services: filebrowser: image: filebrowser/filebrowser:latest container_name: filebrowser - ports: - - "8085:80" + restart: unless-stopped user: "1000:1000" + ports: + - "8085:80" # keep for now (fallback) volumes: - /srv/samba/games:/games - /mnt/storage:/storage - /home/rezayaghobi/docker/filebrowser/config:/config - filebrowser_db:/database - 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: filebrowser_db: filebrowser_srv: +networks: + web_net: + external: true diff --git a/docker-compose/gitlab/docker-compose.yml b/docker-compose/gitlab/docker-compose.yml index f7c3f2a..c15d473 100644 --- a/docker-compose/gitlab/docker-compose.yml +++ b/docker-compose/gitlab/docker-compose.yml @@ -5,9 +5,7 @@ services: restart: unless-stopped hostname: gitlab.local ports: - - "8929:80" - "2224:22" - volumes: - ./config:/etc/gitlab - ./logs:/var/log/gitlab @@ -16,10 +14,14 @@ services: environment: 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'] - external_url 'http://192.168.100.6:8929' + external_url 'https://gitlab.home.server' nginx['listen_port'] = 80 nginx['listen_https'] = false + nginx['proxy_set_headers'] = { + "X-Forwarded-Proto" => "https", + "X-Forwarded-Ssl" => "on" + } gitlab_rails['gitlab_shell_ssh_port'] = 2224 @@ -38,14 +40,52 @@ services: postgres_exporter['enable'] = false gitlab_exporter['enable'] = false - # Registry off for now — flip to true later if you want it - registry['enable'] = false + # Registry + 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: image: gitlab/gitlab-runner:latest container_name: gitlab-runner restart: unless-stopped - ports: - - "9252:9252" volumes: - ./runner-config:/etc/gitlab-runner - /var/run/docker.sock:/var/run/docker.sock + +networks: + web_net: + external: true \ No newline at end of file diff --git a/docker-compose/mediastack/download-client-qbittorent/docker-compose.yml b/docker-compose/mediastack/download-client-qbittorent/docker-compose.yml index 3ddd8fa..fe5739b 100644 --- a/docker-compose/mediastack/download-client-qbittorent/docker-compose.yml +++ b/docker-compose/mediastack/download-client-qbittorent/docker-compose.yml @@ -17,7 +17,25 @@ services: - /mnt/storage/films/downloads:/downloads networks: - 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: mediastack: external: true + web_net: + external: true \ No newline at end of file diff --git a/docker-compose/mediastack/plex/docker-compose.yml b/docker-compose/mediastack/plex/docker-compose.yml index 16daf98..2b4ec59 100644 --- a/docker-compose/mediastack/plex/docker-compose.yml +++ b/docker-compose/mediastack/plex/docker-compose.yml @@ -2,11 +2,12 @@ services: plex: image: lscr.io/linuxserver/plex:latest container_name: plex + restart: unless-stopped network_mode: host + environment: - TZ=Asia/Tehran - - PLEX_CLAIM=claim-uRK14oDz86FsyFgNyaBh + volumes: - /home/rezayaghobi/docker/plex:/config - /mnt/storage:/media - restart: unless-stopped diff --git a/docker-compose/mediastack/prowlarr/docker-compose.yml b/docker-compose/mediastack/prowlarr/docker-compose.yml index cc3ca01..c1752c0 100644 --- a/docker-compose/mediastack/prowlarr/docker-compose.yml +++ b/docker-compose/mediastack/prowlarr/docker-compose.yml @@ -13,7 +13,28 @@ services: - ./config:/config networks: - 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: mediastack: external: true + web_net: + external: true diff --git a/docker-compose/mediastack/radarr/docker-compose.yml b/docker-compose/mediastack/radarr/docker-compose.yml index 39b44b0..84920c3 100644 --- a/docker-compose/mediastack/radarr/docker-compose.yml +++ b/docker-compose/mediastack/radarr/docker-compose.yml @@ -10,13 +10,30 @@ services: - ./config:/config - /mnt/storage/films:/media - /mnt/storage/films/downloads:/downloads - ports: - 7878:7878 restart: unless-stopped networks: - 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: mediastack: external: true + web_net: + external: true \ No newline at end of file diff --git a/docker-compose/mediastack/sonarr/docker-compose.yml b/docker-compose/mediastack/sonarr/docker-compose.yml index 07407a9..5d3be2f 100644 --- a/docker-compose/mediastack/sonarr/docker-compose.yml +++ b/docker-compose/mediastack/sonarr/docker-compose.yml @@ -16,7 +16,29 @@ services: restart: unless-stopped networks: - 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: mediastack: external: true + web_net: + external: true diff --git a/docker-compose/mediastack/whisparr/docker-compose.yml b/docker-compose/mediastack/whisparr/docker-compose.yml index 1b9fd6e..84c49b9 100644 --- a/docker-compose/mediastack/whisparr/docker-compose.yml +++ b/docker-compose/mediastack/whisparr/docker-compose.yml @@ -15,10 +15,27 @@ services: - /mnt/storage/films/downloads:/downloads - /mnt/storage/adult:/data/whisparr - /mnt/storage/films/downloads/whisparr:/downloads/whisparr # ADD THIS - networks: - 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: mediastack: external: true + web_net: + external: true \ No newline at end of file diff --git a/docker-compose/minio/docker-compose.yml b/docker-compose/minio/docker-compose.yml index 13ab361..a7b47d7 100644 --- a/docker-compose/minio/docker-compose.yml +++ b/docker-compose/minio/docker-compose.yml @@ -3,13 +3,54 @@ services: image: minio/minio:RELEASE.2024-01-18T22-51-28Z-cpuv1 container_name: minio restart: unless-stopped - ports: - - "9500:9000" - - "9501:9001" + environment: - MINIO_ROOT_USER: admin - MINIO_ROOT_PASSWORD: change this + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + MINIO_BROWSER_REDIRECT_URL: https://minio.home.server + volumes: - ./data:/data + command: server /data --console-address ":9001" + 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 \ No newline at end of file diff --git a/docker-compose/monitoring-grafana-promethues-cadvisor-node-exporter/docker-compose.yml b/docker-compose/monitoring-grafana-promethues-cadvisor-node-exporter/docker-compose.yml index d6b270a..0e3f014 100644 --- a/docker-compose/monitoring-grafana-promethues-cadvisor-node-exporter/docker-compose.yml +++ b/docker-compose/monitoring-grafana-promethues-cadvisor-node-exporter/docker-compose.yml @@ -4,7 +4,7 @@ services: container_name: prometheus restart: unless-stopped ports: - - "9090:9090" + - "9090:9090" # keep for now volumes: - ./prometheus:/etc/prometheus:ro - ./data/prometheus:/prometheus @@ -14,18 +14,44 @@ services: - "--web.enable-lifecycle" networks: - 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: image: grafana/grafana:latest container_name: grafana restart: unless-stopped ports: - - "3000:3000" + - "3000:3000" # keep for now volumes: - ./grafana/data:/var/lib/grafana - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro networks: - 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: image: prom/node-exporter:latest @@ -39,13 +65,14 @@ services: - /:/rootfs:ro networks: - monitoring + # no Traefik labels – no web UI needed cadvisor: image: ghcr.io/google/cadvisor:latest container_name: cadvisor restart: unless-stopped ports: - - "8080:8080" + - "8080:8080" # keep for now volumes: - /:/rootfs:ro - /var/run:/var/run:rw @@ -53,6 +80,19 @@ services: - /var/lib/docker/:/var/lib/docker:ro networks: - 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: image: grafana/loki:3.7.3 @@ -62,12 +102,25 @@ services: ports: - "3100:3100" volumes: - - ./loki/local-config.yaml:/etc/loki/local-config.yaml:ro + - ./loki/local-config.yaml:/etc/loki/local-config.yaml:ro - ./data/loki:/loki mem_limit: 400m cpus: 1 networks: - 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: image: grafana/alloy:v1.17.1 @@ -91,7 +144,22 @@ services: - loki networks: - 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: monitoring: driver: bridge + web_net: + external: true diff --git a/docker-compose/n8n/docker-compose.yaml b/docker-compose/n8n/docker-compose.yaml index 0bb1036..15204e7 100644 --- a/docker-compose/n8n/docker-compose.yaml +++ b/docker-compose/n8n/docker-compose.yaml @@ -36,23 +36,54 @@ services: DB_POSTGRESDB_USER: ${POSTGRES_USER} 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_BLOCK_ENV_ACCESS_IN_NODE: false N8N_BASIC_AUTH_USER: ${N8N_BASIC_AUTH_USER} N8N_BASIC_AUTH_PASSWORD: ${N8N_BASIC_AUTH_PASSWORD} N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY} N8N_SECURE_COOKIE: false - N8N_HOST: ${N8N_HOST} + N8N_HOST: n8n.home.server + N8N_PROTOCOL: https N8N_PORT: 5678 - N8N_PROTOCOL: http + WEBHOOK_URL: https://n8n.home.server/ 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: - "5678:5678" + networks: + - default # keep the existing n8n_default (for postgres) + - web_net # new + volumes: - ./n8n-data:/home/node/.n8n - /mnt/storage:/mnt/storage - /mnt/windows-video:/mnt/windows-video +networks: + web_net: + external: true diff --git a/docker-compose/openhands/compose.yaml b/docker-compose/openhands/docker-compose.yml similarity index 58% rename from docker-compose/openhands/compose.yaml rename to docker-compose/openhands/docker-compose.yml index 2f19b0b..5ecdec7 100644 --- a/docker-compose/openhands/compose.yaml +++ b/docker-compose/openhands/docker-compose.yml @@ -10,14 +10,19 @@ services: - ./projects:/home/openhands/workspace/project stdin_open: true tty: true - ollama: - image: ollama/ollama:latest - container_name: ollama + depends_on: + - github-mcp + + github-mcp: + image: ghcr.io/github/github-mcp-server:latest + container_name: github-mcp restart: unless-stopped ports: - - "11434:11434" - volumes: - - ollama-data:/root/.ollama + - "8083:8083" + stdin_open: true + tty: true + environment: + - GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN} + volumes: openhands-data: - ollama-data: diff --git a/docker-compose/pihole/.env.example b/docker-compose/pihole/.env.example index 736ddac..667d16a 100644 --- a/docker-compose/pihole/.env.example +++ b/docker-compose/pihole/.env.example @@ -1 +1 @@ -PIHOLE_WEBPASSWORD=change-me +PIHOLE_WEBPASSWORD=CHANGE_ME diff --git a/docker-compose/pihole/docker-compose.yml b/docker-compose/pihole/docker-compose.yml index ee81a57..938bc38 100644 --- a/docker-compose/pihole/docker-compose.yml +++ b/docker-compose/pihole/docker-compose.yml @@ -3,11 +3,10 @@ services: container_name: pihole image: ghcr.io/pi-hole/pihole:latest network_mode: host - env_file: - - .env environment: TZ: "Asia/Tehran" WEBPASSWORD: ${PIHOLE_WEBPASSWORD} + FTLCONF_webserver_port: '8082o,[::]:8082o' volumes: - ./etc-pihole:/etc/pihole - ./etc-dnsmasq.d:/etc/dnsmasq.d diff --git a/docker-compose/portainer/docker-compose.yml b/docker-compose/portainer/docker-compose.yml index 201db32..3ddc03f 100644 --- a/docker-compose/portainer/docker-compose.yml +++ b/docker-compose/portainer/docker-compose.yml @@ -4,7 +4,31 @@ services: container_name: portainer restart: unless-stopped ports: - - "9000:9000" + - "9001:9000" # keep for now (fallback) volumes: - /var/run/docker.sock:/var/run/docker.sock - /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 diff --git a/docker-compose/semaphore/.env.example b/docker-compose/semaphore/.env.example new file mode 100644 index 0000000..5bceff7 --- /dev/null +++ b/docker-compose/semaphore/.env.example @@ -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 diff --git a/docker-compose/semaphore/docker-compose.yml b/docker-compose/semaphore/docker-compose.yml index 3dfd87b..3fd03e9 100644 --- a/docker-compose/semaphore/docker-compose.yml +++ b/docker-compose/semaphore/docker-compose.yml @@ -8,10 +8,32 @@ services: SEMAPHORE_DB_DIALECT: sqlite SEMAPHORE_DB_PATH: /var/lib/semaphore/database.db SEMAPHORE_ADMIN: admin - SEMAPHORE_ADMIN_PASSWORD: choose a passwor or use .env (better ) - SEMAPHORE_ADMIN_EMAIL: rezay5639@gmail.com + SEMAPHORE_ADMIN_PASSWORD: ${SEMAPHORE_ADMIN_PASSWORD} + SEMAPHORE_ADMIN_EMAIL: ${SEMAPHORE_ADMIN_EMAIL} SEMAPHORE_ADMIN_NAME: Admin - SEMAPHORE_SECRET_KEY: + SEMAPHORE_SECRET_KEY: ${SEMAPHORE_SECRET_KEY} volumes: - ./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 + +networks: + web_net: + external: true diff --git a/docker-compose/sentinel/docker-compose.yml b/docker-compose/sentinel/docker-compose.yml index d11e68b..7eb2236 100644 --- a/docker-compose/sentinel/docker-compose.yml +++ b/docker-compose/sentinel/docker-compose.yml @@ -6,7 +6,5 @@ services: volumes: - /dev:/dev - /run/udev:/run/udev:ro - ports: - - "8090:8090" - restart: unless-stopped - + - ./report:/etc/hdsentinel + restart: unless-stopped \ No newline at end of file diff --git a/docker-compose/traefik/README.md b/docker-compose/traefik/README.md new file mode 100644 index 0000000..1783a78 --- /dev/null +++ b/docker-compose/traefik/README.md @@ -0,0 +1,134 @@ +# Traefik + +Self-hosted reverse proxy replacing Nginx Proxy Manager — gives every service a friendly `.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..rule=Host(`.home.server`)" + - "traefik.http.routers..entrypoints=websecure" + - "traefik.http.routers..tls=true" + - "traefik.http.routers..middlewares=security-headers@file" + - "traefik.http.routers.-http.rule=Host(`.home.server`)" + - "traefik.http.routers.-http.entrypoints=web" + - "traefik.http.routers.-http.middlewares=redirect-to-https@file" + - "traefik.http.services..loadbalancer.server.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 `.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) diff --git a/docker-compose/traefik/certs/home.server.crt b/docker-compose/traefik/certs/home.server.crt new file mode 100644 index 0000000..28ffec1 --- /dev/null +++ b/docker-compose/traefik/certs/home.server.crt @@ -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----- diff --git a/docker-compose/traefik/compose.yaml b/docker-compose/traefik/compose.yaml new file mode 100644 index 0000000..d8b5cef --- /dev/null +++ b/docker-compose/traefik/compose.yaml @@ -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 diff --git a/docker-compose/traefik/dynamic/dockscope-auth.yml b/docker-compose/traefik/dynamic/dockscope-auth.yml new file mode 100644 index 0000000..fe114db --- /dev/null +++ b/docker-compose/traefik/dynamic/dockscope-auth.yml @@ -0,0 +1,7 @@ +http: + middlewares: + dockscope-auth: + basicAuth: + users: + # Generate with: htpasswd -nB + - "${DOCKSCOPE_AUTH_USER}:${DOCKSCOPE_AUTH_HASH}" diff --git a/docker-compose/traefik/dynamic/middlewares.yaml b/docker-compose/traefik/dynamic/middlewares.yaml new file mode 100644 index 0000000..1b8664b --- /dev/null +++ b/docker-compose/traefik/dynamic/middlewares.yaml @@ -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" diff --git a/docker-compose/traefik/dynamic/pihole.yaml b/docker-compose/traefik/dynamic/pihole.yaml new file mode 100644 index 0000000..3d47cfd --- /dev/null +++ b/docker-compose/traefik/dynamic/pihole.yaml @@ -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" diff --git a/docker-compose/traefik/dynamic/plex.yml b/docker-compose/traefik/dynamic/plex.yml new file mode 100644 index 0000000..996e969 --- /dev/null +++ b/docker-compose/traefik/dynamic/plex.yml @@ -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" diff --git a/docker-compose/traefik/dynamic/sentinel.yml b/docker-compose/traefik/dynamic/sentinel.yml new file mode 100644 index 0000000..7589e24 --- /dev/null +++ b/docker-compose/traefik/dynamic/sentinel.yml @@ -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" \ No newline at end of file diff --git a/docker-compose/traefik/dynamic/tls.yaml b/docker-compose/traefik/dynamic/tls.yaml new file mode 100644 index 0000000..4862142 --- /dev/null +++ b/docker-compose/traefik/dynamic/tls.yaml @@ -0,0 +1,6 @@ +tls: + stores: + default: + defaultCertificate: + certFile: /certs/home.server.crt + keyFile: /certs/home.server.key diff --git a/docker-compose/vaultwarden/docker-compose.yml b/docker-compose/vaultwarden/docker-compose.yml new file mode 100644 index 0000000..1a8fe55 --- /dev/null +++ b/docker-compose/vaultwarden/docker-compose.yml @@ -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 diff --git a/docs/Home.md b/docs/Home.md index edbd63a..ac136e5 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -13,7 +13,9 @@ Documentation for a self-hosted homelab running entirely on Docker Compose. This - [Storage Health](Storage-Health.md) - [Lessons Learned](Lessons-Learned.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) - [DockScope](DockScope.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 | | **Management** | Portainer, **DockScope** (3D visual dashboard) | | **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 | | **Disk health** | Hard Disk Sentinel | | **AI Agent** | OpenHands + Ollama | diff --git a/docs/Reverse-Proxy.md b/docs/Reverse-Proxy.md index 2896c6b..d0ab4d8 100644 --- a/docs/Reverse-Proxy.md +++ b/docs/Reverse-Proxy.md @@ -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://:` with friendly `.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://:` with friendly `.home` names and trusted local HTTPS. Ran as its own Compose stack under `nginx-proxy-manager/`. ![Sanctions/access error](images/openhands-error.png) diff --git a/docs/Traefik.md b/docs/Traefik.md new file mode 100644 index 0000000..3711d8b --- /dev/null +++ b/docs/Traefik.md @@ -0,0 +1,129 @@ +# Traefik — Reverse Proxy + +Traefik replaced Nginx Proxy Manager as the homelab's reverse proxy, giving every service a `.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 `.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) diff --git a/docs/Vaultwarden.md b/docs/Vaultwarden.md new file mode 100644 index 0000000..551878b --- /dev/null +++ b/docs/Vaultwarden.md @@ -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 +``` + +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)