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
Empty file removed stacks/network/.gitkeep
Empty file.
145 changes: 145 additions & 0 deletions stacks/network/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Network Stack

Network services for HomeLab Stack — DNS ad-blocking, VPN, dynamic DNS, and reverse proxy management.

## What's Included

| Service | Version | URL | Purpose |
|---------|---------|-----|---------|
| AdGuard Home | v0.107.55 | `adguard.<DOMAIN>` | DNS ad-blocker + encrypted DNS |
| WireGuard Easy | 14 | `vpn.<DOMAIN>` | VPN server with web UI |
| Cloudflare DDNS | 1.14.0 | — | Dynamic DNS updater |
| Nginx Proxy Manager | 2.11.3 | `npm.<DOMAIN>` | Reverse proxy management UI |

## Architecture

```
Internet
├──► :53 (DNS) ──► AdGuard Home (DNS filtering + ad block)
├──► :51820 (VPN) ──► WireGuard Easy (VPN tunnel)
▼ Traefik (from base stack)
├──► adguard.<DOMAIN> ──► AdGuard admin UI
├──► vpn.<DOMAIN> ──► WireGuard web UI
└──► npm.<DOMAIN> ──► NPM admin UI

Cloudflare DDNS ──► Updates DNS A record when IP changes
```

## Quick Start

```bash
# From repo root
cp .env.example .env
# Edit .env — set DOMAIN, WG_HOST, CF_API_TOKEN

# Start base stack first
cd stacks/base && docker compose up -d

# Start network stack
cd ../network
ln -sf ../../.env .env
docker compose up -d
```

## Configuration

### Environment Variables

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `DOMAIN` | Yes | — | Base domain |
| `TZ` | No | `Asia/Shanghai` | Timezone |
| `WG_HOST` | Yes | — | Public IP or domain for VPN |
| `WG_PASSWORD_HASH` | No | — | WireGuard UI password (bcrypt hash) |
| `CF_API_TOKEN` | Yes* | — | Cloudflare API token for DDNS |
| `CF_RECORD_NAME` | No | `${DOMAIN}` | DNS record to update |

*CF_API_TOKEN required only if using Cloudflare DDNS.

### AdGuard Home Setup

1. Visit `https://adguard.<DOMAIN>` on first launch
2. Set admin password and listening interfaces
3. Configure upstream DNS (e.g., `https://dns.alidns.com/dns-query` for CN, or `https://dns.quad9.net/dns-query`)
4. Add blocklists: Settings → DNS Blocklists → Add

### WireGuard VPN Setup

1. Visit `https://vpn.<DOMAIN>`
2. Create client profiles (one per device)
3. Scan QR code or download `.conf` file
4. Install WireGuard client on devices (iOS/Android/Windows/Mac)

**Default DNS** points to AdGuard Home (`adguardhome:53`) for ad-free DNS over VPN.

### Cloudflare DDNS

1. Create API token at https://dash.cloudflare.com/profile/api-tokens
2. Required permissions: Zone → DNS → Edit
3. Set `CF_API_TOKEN` in `.env`
4. The container auto-detects your public IP and updates the DNS record

### Nginx Proxy Manager

1. Visit `https://npm.<DOMAIN>`
2. Default login: `admin@example.com` / `changeme`
3. Change password immediately
4. Use for managing additional reverse proxy hosts outside Traefik

## DNS Configuration

Point your domain's nameservers to Cloudflare:

```
yourdomain.com NS → Cloudflare nameservers
*.yourdomain.com A → your server IP (auto-updated by DDNS)
```

AdGuard Home listens on port 53. For local devices, set DNS to your server's LAN IP.

## SSO Integration

All web UIs are protected by Authentik ForwardAuth. To enable:

1. Deploy the SSO stack (`stacks/sso/`)
2. The `authentik-forwardauth@docker` middleware is applied via labels

## CN Network Adaptation

AdGuard Home recommended upstream DNS for CN:
```
https://dns.alidns.com/dns-query
https://doh.pub/dns-query
```

If `CN_MODE=true`, the cloudflare-ddns and wireguard images may need mirror pulls:

```bash
# ghcr.io images need CN mirror
./scripts/cn-pull.sh
```

## Health Check Verification

```bash
# Check all services
docker compose ps --format "table {{.Name}}\t{{.Status}}"

# Individual checks
docker exec adguardhome wget -qO- http://localhost:3000
docker exec wireguard curl -sf http://localhost:51821/
docker exec nginx-proxy-manager curl -sf http://localhost:81/api/
```

## Troubleshooting

| Problem | Solution |
|---------|----------|
| DNS not resolving | Ensure port 53 not in use by host (`sudo systemctl stop systemd-resolved`) |
| WireGuard can't connect | Check `WG_HOST` is correct public IP; open port 51820/udp on firewall |
| DDNS not updating | Verify `CF_API_TOKEN` has DNS Edit permission |
| AdGuard + Traefik conflict | AdGuard uses port 53 only; Traefik uses 80/443 — no conflict |
| NPM shows wrong cert | NPM manages its own certs; for Traefik-managed hosts, use Traefik labels instead |
| VPN clients can't reach LAN | Add `10.0.0.0/8,192.168.0.0/16` to `WG_ALLOWED_IPS` |
73 changes: 68 additions & 5 deletions stacks/network/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,76 @@ services:
ports:
- 53:53/tcp
- 53:53/udp
environment:
- TZ=${TZ:-Asia/Shanghai}
labels:
- traefik.enable=true
- traefik.http.routers.adguard.rule=Host()
- "traefik.http.routers.adguard.rule=Host(`adguard.${DOMAIN}`)"
- traefik.http.routers.adguard.entrypoints=websecure
- traefik.http.routers.adguard.tls=true
- traefik.http.routers.adguard.tls.certresolver=letsencrypt
- traefik.http.services.adguard.loadbalancer.server.port=3000
- traefik.http.routers.adguard.middlewares=authentik-forwardauth@docker
healthcheck:
test: [CMD, wget, -qO-, http://localhost:3000]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s

wireguard:
image: ghcr.io/wg-easy/wg-easy:14
container_name: wireguard
restart: unless-stopped
networks:
- proxy
volumes:
- wireguard-data:/etc/wireguard
ports:
- 51820:51820/udp
environment:
- WG_HOST=${WG_HOST:?WG_HOST is required}
- PASSWORD_HASH=${WG_PASSWORD_HASH:-}
- WG_DEFAULT_DNS=adguardhome
- WG_ALLOWED_IPS=0.0.0.0/0,::/0
- TZ=${TZ:-Asia/Shanghai}
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
labels:
- traefik.enable=true
- "traefik.http.routers.wireguard.rule=Host(`vpn.${DOMAIN}`)"
- traefik.http.routers.wireguard.entrypoints=websecure
- traefik.http.routers.wireguard.tls=true
- traefik.http.routers.wireguard.tls.certresolver=letsencrypt
- traefik.http.services.wireguard.loadbalancer.server.port=51821
- traefik.http.routers.wireguard.middlewares=authentik-forwardauth@docker
healthcheck:
test: [CMD-SHELL, "curl -sf http://localhost:51821/ || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s

cloudflare-ddns:
image: ghcr.io/favonia/cloudflare-ddns:1.14.0
container_name: cloudflare-ddns
restart: unless-stopped
network_mode: host
environment:
- CF_API_TOKEN=${CF_API_TOKEN:?CF_API_TOKEN is required}
- DOMAINS=${CF_RECORD_NAME:-${DOMAIN}}
- PROXIED=false
- TZ=${TZ:-Asia/Shanghai}
healthcheck:
test: [CMD-SHELL, "pgrep cloudflare-ddns || exit 1"]
interval: 60s
timeout: 10s
retries: 3

nginx-proxy-manager:
image: jc21/nginx-proxy-manager:2.11.3
container_name: nginx-proxy-manager
Expand All @@ -32,25 +90,30 @@ services:
volumes:
- npm-data:/data
- npm-letsencrypt:/etc/letsencrypt
ports:
- 8181:81
environment:
- TZ=${TZ:-Asia/Shanghai}
labels:
- traefik.enable=true
- traefik.http.routers.npm.rule=Host()
- "traefik.http.routers.npm.rule=Host(`npm.${DOMAIN}`)"
- traefik.http.routers.npm.entrypoints=websecure
- traefik.http.routers.npm.tls=true
- traefik.http.routers.npm.tls.certresolver=letsencrypt
- traefik.http.services.npm.loadbalancer.server.port=81
- traefik.http.routers.npm.middlewares=authentik-forwardauth@docker
healthcheck:
test: [CMD-SHELL, wget -q --spider http://localhost:3000/api/health || exit 0]
test: [CMD-SHELL, "curl -sf http://localhost:81/api/ || exit 0"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s

networks:
proxy:
external: true

volumes:
adguard-work:
adguard-conf:
wireguard-data:
npm-data:
npm-letsencrypt: