Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ sources.list.bak

# Generated configs
config/traefik/acme.json
config/traefik/.htpasswd
config/traefik/dynamic/*.generated.yml
2 changes: 1 addition & 1 deletion config/traefik/traefik.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
8 changes: 8 additions & 0 deletions scripts/setup-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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!@#%^&*' </dev/urandom | head -c "${1:-32}" || true; }
write_htpasswd() {
local user="$1" hash="$2" htpasswd_file="$ROOT_DIR/config/traefik/.htpasswd"
[[ -z "$user" || -z "$hash" ]] && return 0
mkdir -p "$(dirname "$htpasswd_file")"
printf '%s:%s\n' "$user" "${hash//\$\$/\$}" > "$htpasswd_file"
chmod 600 "$htpasswd_file" 2>/dev/null || true
}

main() {
echo; echo "HomeLab Stack -- Environment Setup"
Expand All @@ -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)")
Expand Down
130 changes: 92 additions & 38 deletions stacks/base/README.md
Original file line number Diff line number Diff line change
@@ -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.<DOMAIN>` | Reverse proxy + TLS termination |
| Portainer CE | 2.21 | `portainer.<DOMAIN>` | Docker management UI |
| Watchtower | latest-stable | — | Automatic container updates |
| Traefik | 3.1.6 | `traefik.<DOMAIN>` | Reverse proxy and TLS termination |
| Portainer CE | 2.21.4 | `portainer.<DOMAIN>` | 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.<DOMAIN> → Portainer
├──► traefik.<DOMAIN> → Traefik Dashboard
└──► *..<DOMAIN> → Other stacks via 'proxy' network

[proxy] ← shared Docker network — all stacks attach here
|
+-- Traefik :80 -> redirects to HTTPS
+-- Traefik :443 -> TLS termination
|
+-- traefik.<DOMAIN> -> Traefik dashboard
+-- portainer.<DOMAIN> -> 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
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://<server-ip>` redirects to HTTPS.
- `https://traefik.<DOMAIN>` requires BasicAuth.
- `https://portainer.<DOMAIN>` opens Portainer.
- Other stacks attached to the `proxy` network are discovered by Traefik only
when they set `traefik.enable=true`.
36 changes: 32 additions & 4 deletions stacks/base/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down