diff --git a/.gitignore b/.gitignore index d3db86ce..8148d420 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ sources.list.bak # Generated configs config/traefik/acme.json +config/traefik/.htpasswd config/traefik/dynamic/*.generated.yml diff --git a/config/traefik/traefik.yml b/config/traefik/traefik.yml index 3e71c28f..032fecf5 100644 --- a/config/traefik/traefik.yml +++ b/config/traefik/traefik.yml @@ -81,7 +81,7 @@ certificatesResolvers: # ----------------------------------------------------------------------------- providers: docker: - endpoint: "unix:///var/run/docker.sock" + endpoint: "tcp://docker-socket-proxy:2375" exposedByDefault: false # Containers must opt-in with traefik.enable=true network: proxy # Default network for routing watch: true diff --git a/install.sh b/install.sh index e911d519..c1c722ba 100644 --- a/install.sh +++ b/install.sh @@ -71,12 +71,23 @@ mkdir -p \ data/vaultwarden chmod 600 config/traefik/acme.json 2>/dev/null || touch config/traefik/acme.json && chmod 600 config/traefik/acme.json +if [[ ! -f config/traefik/.htpasswd ]]; then + user=$(grep -m1 '^TRAEFIK_DASHBOARD_USER=' .env | cut -d= -f2-) + hash=$(grep -m1 '^TRAEFIK_DASHBOARD_PASSWORD_HASH=' .env | cut -d= -f2- | sed 's/\$\$/\$/g') + if [[ -n "$user" && -n "$hash" ]]; then + printf '%s:%s\n' "$user" "$hash" > config/traefik/.htpasswd + chmod 600 config/traefik/.htpasswd + else + log_warn "config/traefik/.htpasswd was not created; Traefik dashboard auth may fail" + fi +fi +docker network inspect proxy >/dev/null 2>&1 || docker network create proxy # --------------------------------------------------------------------------- # Step 5: Launch base infrastructure # --------------------------------------------------------------------------- log_step "Launching base infrastructure" -docker compose -f docker-compose.base.yml up -d +docker compose --env-file .env -f stacks/base/docker-compose.yml up -d log_info "" log_info "${GREEN}${BOLD}✓ Base infrastructure is up!${NC}" diff --git a/scripts/setup-env.sh b/scripts/setup-env.sh index f6faf733..d4e06f6f 100644 --- a/scripts/setup-env.sh +++ b/scripts/setup-env.sh @@ -44,6 +44,13 @@ set_env() { get_env() { grep -m1 "^${1}=" "$ENV_FILE" 2>/dev/null | cut -d= -f2- || true; } gen_secret() { LC_ALL=C tr -dc 'A-Za-z0-9!@#%^&*' "$htpasswd_file" + chmod 600 "$htpasswd_file" 2>/dev/null || true +} main() { echo; echo "HomeLab Stack -- Environment Setup" @@ -66,6 +73,7 @@ main() { log_warn 'htpasswd not found — set TRAEFIK_DASHBOARD_PASSWORD_HASH manually' fi fi + write_htpasswd "$user" "$(get_env TRAEFIK_DASHBOARD_PASSWORD_HASH)" log_step "CN Mirror (Optional)" local cn; cn=$(ask 'Enable CN mirrors? (true/false)' "$(get_env CN_MODE)") diff --git a/stacks/base/README.md b/stacks/base/README.md index 19505b80..7c49959d 100644 --- a/stacks/base/README.md +++ b/stacks/base/README.md @@ -1,76 +1,130 @@ # Base Infrastructure Stack -The foundation of HomeLab Stack. Must be deployed **before any other stack**. +The foundation of HomeLab Stack. Deploy this stack before any other stack. ## What's Included | Service | Version | URL | Purpose | |---------|---------|-----|---------| -| Traefik | 3.1 | `traefik.` | Reverse proxy + TLS termination | -| Portainer CE | 2.21 | `portainer.` | Docker management UI | -| Watchtower | latest-stable | — | Automatic container updates | +| Traefik | 3.1.6 | `traefik.` | Reverse proxy and TLS termination | +| Portainer CE | 2.21.4 | `portainer.` | Docker management UI | +| Watchtower | 1.7.1 | n/a | Automatic container updates | +| Docker Socket Proxy | 0.2.0 | n/a | Constrained Docker API access for Traefik | ## Architecture -``` +```text Internet - │ - ▼ -[Traefik :443] - │ TLS termination (Let's Encrypt) - │ ForwardAuth → Authentik (optional) - │ - ├──► portainer. → Portainer - ├──► traefik. → Traefik Dashboard - └──► *.. → Other stacks via 'proxy' network - -[proxy] ← shared Docker network — all stacks attach here + | + +-- Traefik :80 -> redirects to HTTPS + +-- Traefik :443 -> TLS termination + | + +-- traefik. -> Traefik dashboard + +-- portainer. -> Portainer + +-- other stacks -> containers attached to the proxy network + +Traefik discovers Docker services through docker-socket-proxy instead of +mounting /var/run/docker.sock directly. ``` ## Prerequisites -- Docker >= 24.0 with Compose v2 plugin -- Ports 80 and 443 open on your firewall -- A domain pointing to your server's IP (A record) -- `./scripts/setup-env.sh` completed (creates `.env` and `acme.json`) +- Docker Engine 24 or newer with Docker Compose v2 +- Ports 80 and 443 open on the host firewall +- A domain pointing to the server IP +- A populated root `.env` file ## Quick Start +From the repository root: + ```bash -# From repo root — recommended (runs check-deps + setup-env first) ./install.sh +``` -# Or manually: -cd stacks/base -ln -sf ../../.env .env # share root .env -docker compose up -d +Manual launch: + +```bash +docker network create proxy +touch config/traefik/acme.json +chmod 600 config/traefik/acme.json +./scripts/setup-env.sh +docker compose --env-file .env -f stacks/base/docker-compose.yml up -d ``` ## Configuration -### Environment Variables (`.env`) +### Environment Variables | Variable | Required | Description | |----------|----------|-------------| -| `DOMAIN` | ✅ | Base domain, e.g. `home.example.com` | -| `ACME_EMAIL` | ✅ | Email for Let's Encrypt notifications | -| `TRAEFIK_DASHBOARD_USER` | ✅ | Dashboard login username | -| `TRAEFIK_DASHBOARD_PASSWORD_HASH` | ✅ | Bcrypt hash — see below | -| `TZ` | ✅ | Timezone, e.g. `Asia/Shanghai` | -| `CN_MODE` | — | `true` to use CN Docker mirrors | +| `DOMAIN` | yes | Base domain, for example `home.example.com` | +| `ACME_EMAIL` | yes | Let's Encrypt notification email | +| `TRAEFIK_DASHBOARD_USER` | yes | Dashboard login username | +| `TRAEFIK_DASHBOARD_PASSWORD_HASH` | yes | Bcrypt password hash | +| `TZ` | yes | Timezone, for example `Asia/Shanghai` | -### Generate Dashboard Password Hash +### Dashboard Password + +Generate a bcrypt hash: ```bash -# Install htpasswd (Debian/Ubuntu) sudo apt-get install -y apache2-utils +htpasswd -nbB admin 'yourpassword' | sed 's/\$/\$\$/g' +``` + +Paste the result into `.env` as `TRAEFIK_DASHBOARD_PASSWORD_HASH`. +`scripts/setup-env.sh` also writes `config/traefik/.htpasswd` automatically. -# Generate hash (replace 'yourpassword') -htpasswd -nbB admin 'yourpassword' | sed -e 's/\$$/\$\$\$/g' +If you edit `.env` manually, regenerate the htpasswd file: -# Paste output into .env as TRAEFIK_DASHBOARD_PASSWORD_HASH +```bash +printf '%s:%s\n' "$TRAEFIK_DASHBOARD_USER" \ + "$(printf '%s' "$TRAEFIK_DASHBOARD_PASSWORD_HASH" | sed 's/\$\$/\$/g')" \ + > config/traefik/.htpasswd +chmod 600 config/traefik/.htpasswd ``` ### TLS Certificates -Traefik uses Let's Encrypt HTTP-01 challenge by default. Certificates are stored in \ No newline at end of file +Traefik uses the `letsencrypt` resolver and stores certificates in +`config/traefik/acme.json`. The file must exist and have mode `600` before +Traefik starts. + +The default resolver uses HTTP-01 challenge on port 80. A DNS resolver is also +present in `config/traefik/traefik.yml` for wildcard setups; edit the provider +and credentials before using it. + +### Docker Socket Proxy + +`docker-socket-proxy` exposes only the read-only Docker API endpoints Traefik +needs for service discovery: + +- containers +- events +- info +- networks +- services +- tasks +- version + +`POST=0` is set so Traefik cannot mutate Docker state through the proxy. +Portainer and Watchtower keep direct socket access because those services are +expected to manage containers and update images. + +## Verification + +```bash +docker compose --env-file .env -f stacks/base/docker-compose.yml config +docker compose --env-file .env -f stacks/base/docker-compose.yml up -d +docker compose --env-file .env -f stacks/base/docker-compose.yml ps +``` + +Expected results: + +- Four containers start: Traefik, Portainer, Watchtower, Docker Socket Proxy. +- `http://` redirects to HTTPS. +- `https://traefik.` requires BasicAuth. +- `https://portainer.` opens Portainer. +- Other stacks attached to the `proxy` network are discovered by Traefik only + when they set `traefik.enable=true`. diff --git a/stacks/base/docker-compose.yml b/stacks/base/docker-compose.yml index 7185e836..b0bee551 100644 --- a/stacks/base/docker-compose.yml +++ b/stacks/base/docker-compose.yml @@ -1,7 +1,7 @@ # ============================================================================= # HomeLab Stack — Base Infrastructure # Services: Traefik (reverse proxy) + Portainer (container management) -# + Watchtower (auto-updates) +# + Watchtower (auto-updates) + Docker Socket Proxy # # Prerequisites: # 1. cp .env.example .env && fill in required values @@ -12,6 +12,32 @@ # ============================================================================= services: + # --------------------------------------------------------------------------- + # Docker Socket Proxy - constrained Docker API access for Traefik + # Traefik only needs read-only discovery endpoints. Portainer and Watchtower + # keep direct socket access because they intentionally manage/update Docker. + # --------------------------------------------------------------------------- + docker-socket-proxy: + image: tecnativa/docker-socket-proxy:0.2.0 + container_name: docker-socket-proxy + restart: unless-stopped + networks: + - proxy + environment: + - LOG_LEVEL=warning + - CONTAINERS=1 + - EVENTS=1 + - INFO=1 + - NETWORKS=1 + - SERVICES=1 + - TASKS=1 + - VERSION=1 + - POST=0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + read_only: true + tmpfs: + - /run # --------------------------------------------------------------------------- # Traefik — Reverse Proxy & TLS Termination @@ -29,10 +55,12 @@ services: - "443:443" environment: - TZ=${TZ} + depends_on: + - docker-socket-proxy volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - ../../config/traefik/traefik.yml:/traefik.yml:ro - ../../config/traefik/dynamic:/dynamic:ro + - ../../config/traefik/.htpasswd:/dynamic/.htpasswd:ro - ../../config/traefik/acme.json:/acme.json - traefik-logs:/var/log/traefik labels: @@ -83,7 +111,7 @@ services: # --------------------------------------------------------------------------- # Watchtower — Automatic Container Updates - # Checks for image updates daily at 4:00 AM + # Checks for image updates daily at 3:00 AM # Notifications sent via configured notifier (see .env) # --------------------------------------------------------------------------- watchtower: @@ -96,7 +124,7 @@ services: - TZ=${TZ} - WATCHTOWER_CLEANUP=true - WATCHTOWER_INCLUDE_RESTARTING=true - - WATCHTOWER_SCHEDULE=0 0 4 * * * + - WATCHTOWER_SCHEDULE=0 0 3 * * * - WATCHTOWER_TIMEOUT=60s - WATCHTOWER_NOTIFICATIONS_LEVEL=warn # Scope: only update containers with this label