chore: initial public release
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
# Apps LXC Setup
|
||||
|
||||
## Overview
|
||||
|
||||
The `apps` LXC (10.2.0.60) in VLAN 1020 runs 15+ productivity apps via Docker Compose. All services run behind Authentik SSO (OIDC or forward auth). Shared infrastructure: single Postgres instance + single Redis instance, both local to the LXC. All services use `network_mode: host` to reach shared Postgres/Redis on localhost.
|
||||
|
||||
## LXC Spec
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Hostname | apps |
|
||||
| IP | 10.2.0.60/24 |
|
||||
| Gateway | 10.2.0.1 |
|
||||
| DNS | 10.2.0.11 |
|
||||
| Cores | 4 |
|
||||
| RAM | 6GB |
|
||||
| Disk | 80GB |
|
||||
| Template | debian-12-standard |
|
||||
| Nesting | ✓ (required for Docker) |
|
||||
| Unprivileged | ✓ |
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Port | Domain | DB | Auth |
|
||||
|---------|------|--------|----|------|
|
||||
| Outline | 3000 | outline.lerkolabs.com | Postgres | OIDC |
|
||||
| Gitea | 3001 | gitea.lerkolabs.com | Postgres | OIDC |
|
||||
| Vikunja | 3456 | tasks.lerkolabs.com | Postgres | OIDC |
|
||||
| Ghostfolio | 3333 | finance.lerkolabs.com | Postgres | Forward auth |
|
||||
| Hoarder | 3002 | hoarder.lerkolabs.com | Postgres | Forward auth |
|
||||
| Grist | 8484 | grist.lerkolabs.com | SQLite | Forward auth |
|
||||
| Glance | 8080 | glance.lerkolabs.com | — | Forward auth |
|
||||
| Actual Budget | 5006 | budget.lerkolabs.com | File | Forward auth |
|
||||
| FreshRSS | 8081 | rss.lerkolabs.com | SQLite | Forward auth |
|
||||
| Memos | 5230 | memos.lerkolabs.com | SQLite | Forward auth |
|
||||
| Traggo | 3030 | time.lerkolabs.com | SQLite | Forward auth |
|
||||
| Baikal | 8082 | dav.lerkolabs.com | SQLite | Forward auth |
|
||||
| Filebrowser | 8083 | files.lerkolabs.com | SQLite | Forward auth |
|
||||
| Bytestash | 8084 | — | SQLite | Forward auth |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- LXC created with nesting enabled before first start
|
||||
- Authentik OIDC providers created for Outline, Gitea, Vikunja before starting those services
|
||||
- Caddy Caddyfile updated with all service blocks (see Phase: Caddy)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
apt update && apt upgrade -y
|
||||
apt install -y curl wget git nano ufw
|
||||
timedatectl set-timezone <your/timezone>
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/docker/apps/{shared,outline,gitea,vikunja,ghostfolio,hoarder,grist,glance,actual,freshrss,memos,traggo,baikal,filebrowser,bytestash}
|
||||
```
|
||||
|
||||
## Shared Infrastructure (Postgres + Redis)
|
||||
|
||||
Start this first, before anything else.
|
||||
|
||||
### Shared .env
|
||||
|
||||
```bash
|
||||
# /opt/docker/apps/.env
|
||||
PG_ROOT_PASSWORD= # openssl rand -base64 32
|
||||
REDIS_PASSWORD= # openssl rand -base64 24
|
||||
PG_PASS_OUTLINE= # openssl rand -base64 24
|
||||
PG_PASS_GITEA= # openssl rand -base64 24
|
||||
PG_PASS_VIKUNJA= # openssl rand -base64 24
|
||||
PG_PASS_GHOSTFOLIO= # openssl rand -base64 24
|
||||
PG_PASS_HOARDER= # openssl rand -base64 24
|
||||
PG_PASS_GRIST= # openssl rand -base64 24
|
||||
```
|
||||
|
||||
```bash
|
||||
chmod 600 /opt/docker/apps/.env
|
||||
```
|
||||
|
||||
### shared/docker-compose.yml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:<see private/configs/versions.md>
|
||||
container_name: apps-postgres
|
||||
restart: unless-stopped
|
||||
env_file: /opt/docker/apps/.env
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${PG_ROOT_PASSWORD}
|
||||
POSTGRES_USER: postgres
|
||||
volumes:
|
||||
- ./pgdata:/var/lib/postgresql/data
|
||||
- ./init:/docker-entrypoint-initdb.d
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
networks:
|
||||
- apps-net
|
||||
|
||||
redis:
|
||||
image: redis:<see private/configs/versions.md>
|
||||
container_name: apps-redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD} --save 60 1 --loglevel warning
|
||||
env_file: /opt/docker/apps/.env
|
||||
volumes:
|
||||
- ./redisdata:/data
|
||||
ports:
|
||||
- "127.0.0.1:6379:6379"
|
||||
networks:
|
||||
- apps-net
|
||||
|
||||
networks:
|
||||
apps-net:
|
||||
name: apps-net
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
### Database init script
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/docker/apps/shared/init
|
||||
```
|
||||
|
||||
`/opt/docker/apps/shared/init/01-create-databases.sql`:
|
||||
|
||||
```sql
|
||||
CREATE USER outline WITH PASSWORD 'PG_PASS_OUTLINE_VALUE';
|
||||
CREATE DATABASE outline OWNER outline;
|
||||
|
||||
CREATE USER gitea WITH PASSWORD 'PG_PASS_GITEA_VALUE';
|
||||
CREATE DATABASE gitea OWNER gitea;
|
||||
|
||||
CREATE USER vikunja WITH PASSWORD 'PG_PASS_VIKUNJA_VALUE';
|
||||
CREATE DATABASE vikunja OWNER vikunja;
|
||||
|
||||
CREATE USER ghostfolio WITH PASSWORD 'PG_PASS_GHOSTFOLIO_VALUE';
|
||||
CREATE DATABASE ghostfolio OWNER ghostfolio;
|
||||
|
||||
CREATE USER hoarder WITH PASSWORD 'PG_PASS_HOARDER_VALUE';
|
||||
CREATE DATABASE hoarder OWNER hoarder;
|
||||
|
||||
CREATE USER grist WITH PASSWORD 'PG_PASS_GRIST_VALUE';
|
||||
CREATE DATABASE grist OWNER grist;
|
||||
```
|
||||
|
||||
Replace each `_VALUE` with the actual password from your .env. This script runs once on first `docker compose up`.
|
||||
|
||||
### Start shared infrastructure
|
||||
|
||||
```bash
|
||||
cd /opt/docker/apps/shared
|
||||
docker compose --env-file /opt/docker/apps/.env up -d
|
||||
docker compose logs -f # wait for "ready to accept connections"
|
||||
```
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
docker exec apps-postgres pg_isready -U postgres
|
||||
docker exec apps-postgres psql -U postgres -c "\l"
|
||||
# Should show: outline, gitea, vikunja, ghostfolio, hoarder, grist
|
||||
```
|
||||
|
||||
## Startup Order
|
||||
|
||||
```bash
|
||||
# 1. Shared infrastructure always first
|
||||
cd /opt/docker/apps/shared && docker compose up -d
|
||||
sleep 15 # give postgres time on first run
|
||||
|
||||
# 2. Run Outline migrations before starting Outline
|
||||
cd /opt/docker/apps/outline && docker compose run --rm outline-migrate
|
||||
docker compose up -d outline
|
||||
|
||||
# 3. Everything else in parallel
|
||||
for svc in gitea vikunja ghostfolio hoarder grist glance actual freshrss memos traggo baikal filebrowser bytestash; do
|
||||
cd /opt/docker/apps/$svc && docker compose up -d
|
||||
done
|
||||
```
|
||||
|
||||
## Caddy Configuration
|
||||
|
||||
Add to Caddyfile on the infra LXC (10.2.0.20). Services with native OIDC don't need forward auth; others do.
|
||||
|
||||
```caddyfile
|
||||
# OIDC-native (no forward auth needed)
|
||||
outline.lerkolabs.com {
|
||||
reverse_proxy 10.2.0.60:3000
|
||||
}
|
||||
gitea.lerkolabs.com {
|
||||
reverse_proxy 10.2.0.60:3001
|
||||
}
|
||||
tasks.lerkolabs.com {
|
||||
reverse_proxy 10.2.0.60:3456
|
||||
}
|
||||
|
||||
# Forward auth
|
||||
finance.lerkolabs.com {
|
||||
import authentik_forward_auth
|
||||
reverse_proxy 10.2.0.60:3333
|
||||
}
|
||||
# ... repeat pattern for remaining services
|
||||
```
|
||||
|
||||
## Pi-hole DNS Records
|
||||
|
||||
All records point to 10.2.0.20 (Caddy), not 10.2.0.60 directly:
|
||||
|
||||
```
|
||||
outline.lerkolabs.com → 10.2.0.20
|
||||
gitea.lerkolabs.com → 10.2.0.20
|
||||
tasks.lerkolabs.com → 10.2.0.20
|
||||
finance.lerkolabs.com → 10.2.0.20
|
||||
hoarder.lerkolabs.com → 10.2.0.20
|
||||
grist.lerkolabs.com → 10.2.0.20
|
||||
glance.lerkolabs.com → 10.2.0.20
|
||||
budget.lerkolabs.com → 10.2.0.20
|
||||
rss.lerkolabs.com → 10.2.0.20
|
||||
memos.lerkolabs.com → 10.2.0.20
|
||||
time.lerkolabs.com → 10.2.0.20
|
||||
dav.lerkolabs.com → 10.2.0.20
|
||||
files.lerkolabs.com → 10.2.0.20
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# All containers running
|
||||
docker ps --format "table {{.Names}}\t{{.Status}}" | sort
|
||||
|
||||
# Outline health
|
||||
curl -s http://localhost:3000/api/health
|
||||
|
||||
# From LAN — check Authentik gate works
|
||||
curl -I https://outline.lerkolabs.com
|
||||
```
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```bash
|
||||
# Logs for a service
|
||||
docker logs -f outline
|
||||
|
||||
# Postgres: connect to a database
|
||||
docker exec -it apps-postgres psql -U postgres -d outline
|
||||
|
||||
# Postgres: backup
|
||||
docker exec apps-postgres pg_dump -U postgres outline > /opt/backups/outline-$(date +%Y%m%d).sql
|
||||
|
||||
# Disk usage by service
|
||||
du -sh /opt/docker/apps/*/
|
||||
```
|
||||
@@ -0,0 +1,227 @@
|
||||
# Authentik Setup
|
||||
|
||||
## Overview
|
||||
|
||||
Authentik is the centralized identity provider for the entire homelab. It runs in the `auth` LXC (10.2.0.25) in VLAN 1020. It provides two auth mechanisms:
|
||||
|
||||
1. **Forward auth** — Caddy asks Authentik "is this person logged in?" before proxying any request. Services that don't support SSO natively get a login wall this way.
|
||||
2. **OIDC provider** — Services that support OAuth2/OIDC (Outline, Gitea, Vikunja, Grafana, Proxmox) get true single sign-on.
|
||||
|
||||
Stack: Postgres + Authentik server + Authentik worker (Redis removed as of 2025.10).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- LXC at 10.2.0.25 with nesting enabled (Docker requires it)
|
||||
- Caddy already deployed at 10.2.0.20
|
||||
- Pi-hole DNS record: `auth.lerkolabs.com → 10.2.0.20`
|
||||
|
||||
## LXC Spec
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Hostname | auth |
|
||||
| IP | 10.2.0.25/24 |
|
||||
| Gateway | 10.2.0.1 |
|
||||
| Cores | 2 |
|
||||
| RAM | 2GB |
|
||||
| Disk | 10GB |
|
||||
| Template | debian-12-standard |
|
||||
| Nesting | ✓ |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
apt update && apt upgrade -y
|
||||
apt install -y curl nano
|
||||
timedatectl set-timezone <your/timezone>
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker
|
||||
mkdir -p /opt/docker/authentik/{data,certs,custom-templates}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Generate secrets and create .env
|
||||
|
||||
```bash
|
||||
echo "PG_PASS=$(openssl rand -base64 36 | tr -d '\n')" >> /opt/docker/authentik/.env
|
||||
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60 | tr -d '\n')" >> /opt/docker/authentik/.env
|
||||
```
|
||||
|
||||
Add remaining config:
|
||||
|
||||
```bash
|
||||
# PostgreSQL
|
||||
PG_USER=authentik
|
||||
PG_DB=authentik
|
||||
|
||||
# Pin to current version
|
||||
AUTHENTIK_TAG=<see private/configs/versions.md>
|
||||
|
||||
AUTHENTIK_LOG_LEVEL=info
|
||||
```
|
||||
|
||||
```bash
|
||||
chmod 600 /opt/docker/authentik/.env
|
||||
```
|
||||
|
||||
### docker-compose.yml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgresql:
|
||||
image: docker.io/library/postgres:<see private/configs/versions.md>
|
||||
container_name: authentik-db
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d ${PG_DB} -U ${PG_USER}"]
|
||||
start_period: 20s
|
||||
interval: 30s
|
||||
retries: 5
|
||||
timeout: 5s
|
||||
volumes:
|
||||
- ./postgres:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${PG_PASS}
|
||||
POSTGRES_USER: ${PG_USER}
|
||||
POSTGRES_DB: ${PG_DB}
|
||||
|
||||
server:
|
||||
image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG}
|
||||
container_name: authentik-server
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER}
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB}
|
||||
AUTHENTIK_LOG_LEVEL: ${AUTHENTIK_LOG_LEVEL:-info}
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./custom-templates:/templates
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9443:9443"
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
|
||||
worker:
|
||||
image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG}
|
||||
container_name: authentik-worker
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER}
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB}
|
||||
AUTHENTIK_LOG_LEVEL: ${AUTHENTIK_LOG_LEVEL:-info}
|
||||
user: root
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./data:/data
|
||||
- ./certs:/certs
|
||||
- ./custom-templates:/templates
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
```
|
||||
|
||||
## Start Authentik
|
||||
|
||||
```bash
|
||||
cd /opt/docker/authentik
|
||||
docker compose up -d
|
||||
docker logs -f authentik-server
|
||||
# Wait for: "Everything is ready"
|
||||
```
|
||||
|
||||
## Initial Admin Setup
|
||||
|
||||
Navigate to: `http://10.2.0.25:9000/if/flow/initial-setup/` (include trailing slash)
|
||||
|
||||
Set admin password for the `akadmin` account. Save in Vaultwarden.
|
||||
|
||||
## Caddy Integration
|
||||
|
||||
Add to Caddyfile on the infra LXC (see [caddy.md](caddy.md)):
|
||||
|
||||
```caddyfile
|
||||
# Forward auth snippet
|
||||
(authentik_forward_auth) {
|
||||
forward_auth 10.2.0.25:9000 {
|
||||
uri /outpost.goauthentik.io/auth/caddy
|
||||
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email \
|
||||
X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks \
|
||||
X-Authentik-Meta-Outpost X-Authentik-Meta-Provider \
|
||||
X-Authentik-Meta-App X-Authentik-Meta-Version
|
||||
trusted_proxies private_ranges
|
||||
}
|
||||
}
|
||||
|
||||
auth.lerkolabs.com {
|
||||
reverse_proxy 10.2.0.25:9000
|
||||
}
|
||||
```
|
||||
|
||||
## Configure Outpost
|
||||
|
||||
In Authentik admin: Applications → Outposts → authentik Embedded Outpost → Edit
|
||||
|
||||
Set both:
|
||||
- `authentik Host`: `https://auth.lerkolabs.com`
|
||||
- `authentik Host (browser)`: `https://auth.lerkolabs.com`
|
||||
|
||||
## Adding Applications
|
||||
|
||||
### Forward auth pattern (services without native OIDC)
|
||||
|
||||
1. Applications → Providers → Create → Proxy Provider
|
||||
- Mode: Forward auth (single application)
|
||||
- External host: `https://<service>.lerkolabs.com`
|
||||
2. Applications → Applications → Create
|
||||
- Assign provider, set slug and launch URL
|
||||
3. Outposts → Embedded Outpost → Edit → add application to Selected Applications
|
||||
|
||||
### OIDC pattern (Outline, Gitea, Vikunja)
|
||||
|
||||
1. Applications → Providers → Create → OAuth2/OpenID Provider
|
||||
- Client type: Confidential
|
||||
- Redirect URI: service-specific (see table below)
|
||||
- Scopes: openid, email, profile
|
||||
2. Note the Client ID and Client Secret — configure in the service's .env
|
||||
|
||||
| Service | Redirect URI |
|
||||
|---------|-------------|
|
||||
| Outline | `https://outline.lerkolabs.com/auth/oidc.callback` |
|
||||
| Gitea | `https://gitea.lerkolabs.com/user/oauth2/authentik/callback` |
|
||||
| Vikunja | `https://tasks.lerkolabs.com/auth/openid/authentik` |
|
||||
| Grafana | `https://grafana.lerkolabs.com/login/generic_oauth` |
|
||||
|
||||
OIDC discovery URL pattern: `https://auth.lerkolabs.com/application/o/<slug>/.well-known/openid-configuration`
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# All containers healthy
|
||||
docker ps
|
||||
# authentik-db Up X minutes (healthy)
|
||||
# authentik-server Up X minutes
|
||||
# authentik-worker Up X minutes
|
||||
|
||||
# Accessible via Caddy
|
||||
curl -I https://auth.lerkolabs.com
|
||||
# Expected: HTTP/2 302 (redirect to login)
|
||||
```
|
||||
|
||||
## Updates
|
||||
|
||||
```bash
|
||||
# Edit .env, bump AUTHENTIK_TAG to new version
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
# Caddy (infra LXC) Setup
|
||||
|
||||
## Overview
|
||||
|
||||
The `infra` LXC (10.2.0.20) in VLAN 1020 runs Caddy as the internal reverse proxy. It handles TLS termination for all `*.lerkolabs.com` services using wildcard certs via Cloudflare DNS-01 challenge. Also hosts ntfy (push notifications) and Uptime Kuma (service monitoring) as co-located services.
|
||||
|
||||
| Service | Port | Domain |
|
||||
|---------|------|--------|
|
||||
| Caddy | 80/443 | reverse proxy — no direct domain |
|
||||
| ntfy | 8090 | ntfy.lerkolabs.com |
|
||||
| Uptime Kuma | 3001 | uptime.lerkolabs.com |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- LXC created at 10.2.0.20 in VLAN 1020
|
||||
- Cloudflare API token with Zone → DNS → Edit permissions for lerkolabs.com (stored in Vaultwarden)
|
||||
- Pi-hole DNS record: `*.lerkolabs.com → 10.2.0.20`
|
||||
|
||||
## LXC Spec
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Hostname | infra |
|
||||
| IP | 10.2.0.20/24 |
|
||||
| Gateway | 10.2.0.1 |
|
||||
| Cores | 2 |
|
||||
| RAM | 1GB |
|
||||
| Template | debian-12-standard |
|
||||
| Nesting | ✓ (required for Docker) |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
apt update && apt upgrade -y
|
||||
apt install -y curl nano ufw
|
||||
timedatectl set-timezone <your/timezone>
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
/opt/docker/
|
||||
├── caddy/
|
||||
│ ├── Caddyfile
|
||||
│ ├── docker-compose.yml
|
||||
│ ├── Dockerfile
|
||||
│ ├── .env
|
||||
│ ├── data/
|
||||
│ ├── config/
|
||||
│ └── logs/
|
||||
└── infra/
|
||||
├── uptimekuma/
|
||||
│ ├── docker-compose.yml
|
||||
│ └── data/
|
||||
└── ntfy/
|
||||
├── docker-compose.yml
|
||||
├── server.yml
|
||||
└── data/
|
||||
```
|
||||
|
||||
## Caddy Deployment
|
||||
|
||||
### Dockerfile (custom build with Cloudflare DNS plugin)
|
||||
|
||||
```dockerfile
|
||||
FROM caddy:<see private/configs/versions.md> AS builder
|
||||
RUN xcaddy build \
|
||||
--with github.com/caddy-dns/cloudflare
|
||||
|
||||
FROM caddy:<see private/configs/versions.md>
|
||||
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||
```
|
||||
|
||||
### .env
|
||||
|
||||
```bash
|
||||
CLOUDFLARE_API_TOKEN=<token from Vaultwarden: homelab/cloudflare-api>
|
||||
```
|
||||
|
||||
```bash
|
||||
chmod 600 /opt/docker/caddy/.env
|
||||
```
|
||||
|
||||
### docker-compose.yml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
caddy:
|
||||
build: .
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ./data:/data
|
||||
- ./config:/config
|
||||
- ./logs:/logs
|
||||
env_file:
|
||||
- .env
|
||||
```
|
||||
|
||||
### Caddyfile structure
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
email <your-acme-email>
|
||||
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
|
||||
}
|
||||
|
||||
# Forward auth snippet — reuse in every protected service block
|
||||
(authentik_forward_auth) {
|
||||
forward_auth 10.2.0.25:9000 {
|
||||
uri /outpost.goauthentik.io/auth/caddy
|
||||
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email \
|
||||
X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks \
|
||||
X-Authentik-Meta-Outpost X-Authentik-Meta-Provider \
|
||||
X-Authentik-Meta-App X-Authentik-Meta-Version
|
||||
trusted_proxies private_ranges
|
||||
}
|
||||
}
|
||||
|
||||
# Authentik
|
||||
auth.lerkolabs.com {
|
||||
reverse_proxy 10.2.0.25:9000
|
||||
}
|
||||
|
||||
# Pi-hole (forward auth)
|
||||
pihole.lerkolabs.com {
|
||||
import authentik_forward_auth
|
||||
reverse_proxy 10.2.0.11:80
|
||||
}
|
||||
|
||||
# Add remaining services following the same pattern
|
||||
# Services with native OIDC (Outline, Gitea, Vikunja): no forward auth needed
|
||||
# Services without OIDC: import authentik_forward_auth
|
||||
```
|
||||
|
||||
### Build and start
|
||||
|
||||
```bash
|
||||
cd /opt/docker/caddy
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
docker logs -f caddy
|
||||
# Wait for: "certificate obtained successfully"
|
||||
```
|
||||
|
||||
## ntfy Deployment
|
||||
|
||||
### server.yml
|
||||
|
||||
```yaml
|
||||
base-url: https://ntfy.lerkolabs.com
|
||||
listen-http: ":8090"
|
||||
cache-file: /var/cache/ntfy/cache.db
|
||||
auth-file: /var/lib/ntfy/auth.db
|
||||
auth-default-access: deny-all
|
||||
behind-proxy: true
|
||||
```
|
||||
|
||||
### docker-compose.yml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
ntfy:
|
||||
image: binwiederhier/ntfy:latest
|
||||
container_name: ntfy
|
||||
restart: unless-stopped
|
||||
command: serve
|
||||
ports:
|
||||
- "8090:8090"
|
||||
volumes:
|
||||
- ./server.yml:/etc/ntfy/server.yml:ro
|
||||
- ./data:/var/cache/ntfy
|
||||
- ./data:/var/lib/ntfy
|
||||
```
|
||||
|
||||
```bash
|
||||
cd /opt/docker/infra/ntfy && docker compose up -d
|
||||
docker exec -it ntfy ntfy user add --role=admin <username>
|
||||
```
|
||||
|
||||
## Uptime Kuma Deployment
|
||||
|
||||
```yaml
|
||||
services:
|
||||
uptime-kuma:
|
||||
image: louislam/uptime-kuma:latest
|
||||
container_name: uptime-kuma
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3001:3001"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
```
|
||||
|
||||
```bash
|
||||
cd /opt/docker/infra/uptimekuma && docker compose up -d
|
||||
```
|
||||
|
||||
## Caddy Reload
|
||||
|
||||
After editing the Caddyfile:
|
||||
|
||||
```bash
|
||||
docker exec caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
docker exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# Cert issued
|
||||
docker logs caddy | grep "certificate obtained"
|
||||
|
||||
# Service reachable
|
||||
curl -I https://pihole.lerkolabs.com
|
||||
# Expected: HTTP/2 200
|
||||
|
||||
# Ports bound
|
||||
ss -tlnp | grep -E "443|8090|3001"
|
||||
```
|
||||
@@ -0,0 +1,157 @@
|
||||
# Monitor LXC Setup
|
||||
|
||||
## Overview
|
||||
|
||||
The `monitor` LXC (10.2.0.51) in VLAN 1020 runs the full observability stack: Victoria Metrics (metrics storage), Grafana (dashboards and alerting), and Beszel (container + host monitoring). All services run via Docker Compose.
|
||||
|
||||
## LXC Spec
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Hostname | monitor |
|
||||
| IP | 10.2.0.51/24 |
|
||||
| Gateway | 10.2.0.1 |
|
||||
| DNS | 10.2.0.11 |
|
||||
| Cores | 4 |
|
||||
| RAM | 4GB |
|
||||
| Template | debian-12-standard |
|
||||
| Nesting | ✓ |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Caddy running at 10.2.0.20
|
||||
- Pi-hole DNS records added (see Verification)
|
||||
- Beszel agents deployed on all LXCs to be monitored
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
apt update && apt upgrade -y
|
||||
apt install -y curl nano
|
||||
timedatectl set-timezone <your/timezone>
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker
|
||||
mkdir -p /opt/docker/monitor/{victoria-metrics,grafana,beszel}
|
||||
```
|
||||
|
||||
## Victoria Metrics
|
||||
|
||||
```yaml
|
||||
# /opt/docker/monitor/victoria-metrics/docker-compose.yml
|
||||
services:
|
||||
victoria-metrics:
|
||||
image: victoriametrics/victoria-metrics:latest
|
||||
container_name: victoria-metrics
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8428:8428"
|
||||
volumes:
|
||||
- ./data:/storage
|
||||
command:
|
||||
- "--storageDataPath=/storage"
|
||||
- "--retentionPeriod=90d"
|
||||
```
|
||||
|
||||
```bash
|
||||
cd /opt/docker/monitor/victoria-metrics && docker compose up -d
|
||||
```
|
||||
|
||||
## Grafana
|
||||
|
||||
```yaml
|
||||
# /opt/docker/monitor/grafana/docker-compose.yml
|
||||
services:
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./data:/var/lib/grafana
|
||||
environment:
|
||||
GF_SERVER_ROOT_URL: https://grafana.lerkolabs.com
|
||||
GF_AUTH_GENERIC_OAUTH_ENABLED: "true"
|
||||
GF_AUTH_GENERIC_OAUTH_NAME: Authentik
|
||||
GF_AUTH_GENERIC_OAUTH_CLIENT_ID: <from Authentik OIDC provider>
|
||||
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: <from Authentik OIDC provider>
|
||||
GF_AUTH_GENERIC_OAUTH_SCOPES: openid email profile
|
||||
GF_AUTH_GENERIC_OAUTH_AUTH_URL: https://auth.lerkolabs.com/application/o/authorize/
|
||||
GF_AUTH_GENERIC_OAUTH_TOKEN_URL: https://auth.lerkolabs.com/application/o/token/
|
||||
GF_AUTH_GENERIC_OAUTH_API_URL: https://auth.lerkolabs.com/application/o/userinfo/
|
||||
GF_AUTH_SIGNOUT_REDIRECT_URL: https://auth.lerkolabs.com/application/o/grafana/end-session/
|
||||
GF_AUTH_OAUTH_AUTO_LOGIN: "true"
|
||||
```
|
||||
|
||||
```bash
|
||||
cd /opt/docker/monitor/grafana && docker compose up -d
|
||||
```
|
||||
|
||||
Add Victoria Metrics as a data source in Grafana: `http://localhost:8428`
|
||||
|
||||
## Beszel
|
||||
|
||||
Beszel hub runs on the monitor LXC. Beszel agents run on each LXC/VM being monitored.
|
||||
|
||||
### Hub (monitor LXC)
|
||||
|
||||
```yaml
|
||||
# /opt/docker/monitor/beszel/docker-compose.yml
|
||||
services:
|
||||
beszel:
|
||||
image: henrygd/beszel:latest
|
||||
container_name: beszel
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8090:8090"
|
||||
volumes:
|
||||
- ./data:/beszel_data
|
||||
```
|
||||
|
||||
```bash
|
||||
cd /opt/docker/monitor/beszel && docker compose up -d
|
||||
```
|
||||
|
||||
### Agents (each LXC)
|
||||
|
||||
On each LXC that needs monitoring:
|
||||
|
||||
```bash
|
||||
curl -sL https://raw.githubusercontent.com/henrygd/beszel/main/supplemental/scripts/install-agent.sh -o install-agent.sh
|
||||
chmod +x install-agent.sh
|
||||
./install-agent.sh # follow prompts, enter hub address and key
|
||||
```
|
||||
|
||||
## Caddy Configuration
|
||||
|
||||
Add to Caddyfile on infra LXC:
|
||||
|
||||
```caddyfile
|
||||
grafana.lerkolabs.com {
|
||||
reverse_proxy 10.2.0.51:3000
|
||||
}
|
||||
```
|
||||
|
||||
Beszel and Victoria Metrics are internal-only (no public Caddy entries needed unless you want external access).
|
||||
|
||||
## Pi-hole DNS Records
|
||||
|
||||
```
|
||||
grafana.lerkolabs.com → 10.2.0.20
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# All containers running
|
||||
docker ps
|
||||
|
||||
# Victoria Metrics health
|
||||
curl http://localhost:8428/health
|
||||
|
||||
# Grafana reachable
|
||||
curl -I https://grafana.lerkolabs.com
|
||||
|
||||
# Beszel agents reporting
|
||||
# Check Beszel web UI at http://10.2.0.51:8090
|
||||
```
|
||||
@@ -0,0 +1,116 @@
|
||||
# pfSense VLAN Setup
|
||||
|
||||
## Overview
|
||||
|
||||
pfSense (Intel N100 mini PC at 10.0.0.1 / 10.1.0.1) handles firewall, routing, DHCP, DNS resolution, and WireGuard VPN for all 8 VLANs. See [Network](../docs/NETWORK.md) for the full VLAN map and firewall policy. See [Decisions](../docs/DECISIONS.md) D005 for the AT&T IP Passthrough rationale.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- pfSense installed on Intel N100 mini PC
|
||||
- AT&T BGW320 in IP Passthrough mode (pfSense WAN gets public IP)
|
||||
- Omada managed switch connected to pfSense
|
||||
- Trunk port between pfSense and switch carrying all VLANs tagged
|
||||
|
||||
## VLAN Configuration
|
||||
|
||||
### 1. Create VLAN Interfaces
|
||||
|
||||
Navigate to: **Interfaces → VLANs → Add**
|
||||
|
||||
Create one entry per VLAN:
|
||||
|
||||
| VLAN Tag | Parent | Description |
|
||||
|----------|--------|-------------|
|
||||
| 1000 | (WAN NIC or LAN NIC) | MGMT |
|
||||
| 1010 | (LAN NIC) | LAN |
|
||||
| 1020 | (LAN NIC) | Homelab |
|
||||
| 1030 | (LAN NIC) | Guests |
|
||||
| 1040 | (LAN NIC) | IoT |
|
||||
| 1050 | (LAN NIC) | WFH |
|
||||
| 1099 | (LAN NIC) | DMZ |
|
||||
|
||||
### 2. Assign VLAN Interfaces
|
||||
|
||||
Navigate to: **Interfaces → Assignments**
|
||||
|
||||
Add each VLAN as a new interface. Enable and configure each:
|
||||
|
||||
| Interface | IP | Subnet |
|
||||
|-----------|-----|--------|
|
||||
| MGMT (1000) | 10.0.0.1 | /24 |
|
||||
| LAN (1010) | 10.1.0.1 | /24 |
|
||||
| Homelab (1020) | 10.2.0.1 | /24 |
|
||||
| Guests (1030) | 10.3.0.1 | /24 |
|
||||
| IoT (1040) | 10.4.0.1 | /24 |
|
||||
| WFH (1050) | 10.5.0.1 | /24 |
|
||||
| DMZ (1099) | 10.99.0.1 | /24 |
|
||||
|
||||
### 3. DHCP Servers
|
||||
|
||||
Navigate to: **Services → DHCP Server** — configure one per VLAN:
|
||||
|
||||
| VLAN | DHCP Range | DNS |
|
||||
|------|------------|-----|
|
||||
| MGMT | 10.0.0.100–150 | pfSense (10.0.0.1) |
|
||||
| LAN | 10.1.0.100–200 | Pi-hole (10.2.0.11) |
|
||||
| Homelab | 10.2.0.100–200 | Pi-hole (10.2.0.11) |
|
||||
| Guests | 10.3.0.100–250 | Pi-hole (10.2.0.11) |
|
||||
| IoT | 10.4.0.100–250 | Pi-hole (10.2.0.11) |
|
||||
| WFH | 10.5.0.100–200 | pfSense (10.5.0.1) — Pi-hole intentionally excluded |
|
||||
| DMZ | static only | pfSense (10.99.0.1) |
|
||||
|
||||
### 4. Firewall Rules
|
||||
|
||||
Navigate to: **Firewall → Rules** — configure per-interface rules following the policy in [NETWORK.md](../docs/NETWORK.md#firewall-policy).
|
||||
|
||||
Key rules:
|
||||
|
||||
- Default deny all inter-VLAN (floating rule or per-interface block at end)
|
||||
- LAN → Homelab: allow (LAN users reach services)
|
||||
- LAN → MGMT: allow (admin access from home devices)
|
||||
- Homelab → internet: HTTP/S, SSH, NTP only (for updates)
|
||||
- Guests → internet only: block all RFC1918
|
||||
- IoT → internet + Home Assistant: block everything else
|
||||
- WFH → internet only: block all RFC1918, pfSense DNS only
|
||||
- MGMT → internet: NTP + updates only; inbound from LAN + VPN only
|
||||
- DMZ → internet: HTTP/S + NTP; block all internal VLANs
|
||||
|
||||
### 5. DNS Resolver (Unbound)
|
||||
|
||||
Navigate to: **Services → DNS Resolver**
|
||||
|
||||
- Enable: ✓
|
||||
- Listen on: all interfaces
|
||||
- Upstream DNS: Cloudflare 1.1.1.1
|
||||
- DNSSEC: ✓ (optional)
|
||||
|
||||
Pi-hole (10.2.0.11) uses pfSense Unbound as its upstream. WFH VLAN devices use pfSense Unbound directly — Pi-hole is unreachable from WFH by firewall rule.
|
||||
|
||||
### 6. Static DHCP Reservations
|
||||
|
||||
Navigate to: **Services → DHCP Server → [interface] → DHCP Static Mappings**
|
||||
|
||||
Add reservations for all homelab hosts from [NETWORK.md](../docs/NETWORK.md#static-ip-reservations).
|
||||
|
||||
## Configuration Backup
|
||||
|
||||
Navigate to: **Diagnostics → Backup & Restore → Backup Configuration**
|
||||
|
||||
Download `config.xml`. Store in Vaultwarden or PBS. This is the single file needed to restore pfSense from scratch.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# From a LAN device:
|
||||
# 1. Gets IP from DHCP in 10.1.0.100–200 range
|
||||
ip addr
|
||||
|
||||
# 2. DNS resolves via Pi-hole
|
||||
nslookup google.com # should show answer from 10.2.0.11
|
||||
|
||||
# 3. Internal service resolves
|
||||
nslookup outline.lerkolabs.com # should return 10.2.0.20
|
||||
|
||||
# 4. Internet access works
|
||||
curl -I https://google.com
|
||||
```
|
||||
@@ -0,0 +1,96 @@
|
||||
# Pi-hole Setup
|
||||
|
||||
## Overview
|
||||
|
||||
Pi-hole runs in the `pihole` LXC (10.2.0.11) in VLAN 1020 (Homelab). It is the primary DNS server for all VLANs, providing ad/tracker blocking, local DNS records, and query logging. All `*.lerkolabs.com` subdomains resolve to 10.2.0.20 (Caddy). Upstream resolver is pfSense Unbound → Cloudflare 1.1.1.1.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- LXC created in VLAN 1020 with static IP 10.2.0.11
|
||||
- Debian 12 template
|
||||
- pfSense DHCP reservations updated to point VLANs at 10.2.0.11 for DNS
|
||||
|
||||
## LXC Spec
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Hostname | pihole |
|
||||
| IP | 10.2.0.11/24 |
|
||||
| Gateway | 10.2.0.1 |
|
||||
| Cores | 1 |
|
||||
| RAM | 512MB |
|
||||
| Template | debian-12-standard |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
apt update && apt upgrade -y
|
||||
curl -sSL https://install.pi-hole.net | bash
|
||||
```
|
||||
|
||||
Installer prompts:
|
||||
- Upstream DNS: Custom (set to pfSense: 10.2.0.1)
|
||||
- Blocklists: Default (customize later)
|
||||
- Admin Web Interface: Yes
|
||||
- Web Server: lighttpd
|
||||
- Query Logging: Yes
|
||||
- Privacy Mode: Show everything (0)
|
||||
|
||||
## Configuration
|
||||
|
||||
### Local DNS Records
|
||||
|
||||
Add all internal domains via **Local DNS → DNS Records**. Every entry points to 10.2.0.20 (Caddy), not the service directly.
|
||||
|
||||
Key records to add:
|
||||
|
||||
| Domain | IP |
|
||||
|--------|----|
|
||||
| pihole.lerkolabs.com | 10.2.0.20 |
|
||||
| auth.lerkolabs.com | 10.2.0.20 |
|
||||
| outline.lerkolabs.com | 10.2.0.20 |
|
||||
| gitea.lerkolabs.com | 10.2.0.20 |
|
||||
| tasks.lerkolabs.com | 10.2.0.20 |
|
||||
| finance.lerkolabs.com | 10.2.0.20 |
|
||||
| grafana.lerkolabs.com | 10.2.0.20 |
|
||||
| proxmox.lerkolabs.com | 10.2.0.20 |
|
||||
| vault.lerkolabs.com | 10.2.0.20 |
|
||||
|
||||
Add remaining services from [SERVICES.md](../docs/SERVICES.md) following the same pattern.
|
||||
|
||||
### Upstream DNS
|
||||
|
||||
Settings → DNS → Custom upstream: `10.2.0.1` (pfSense Unbound)
|
||||
|
||||
Uncheck all other upstream providers.
|
||||
|
||||
### pfSense DHCP Integration
|
||||
|
||||
In pfSense: set DNS server for each VLAN's DHCP scope to 10.2.0.11. The WFH VLAN (1050) is the exception — it uses pfSense DNS only (Pi-hole unreachable by design).
|
||||
|
||||
## Backup / Restore
|
||||
|
||||
Use Teleporter for full config export: Settings → Teleporter → Backup. Store the teleporter zip in Vaultwarden or PBS.
|
||||
|
||||
On restore: Settings → Teleporter → Restore. All DNS records, blocklists, and settings are included.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# DNS resolves internal names
|
||||
nslookup outline.lerkolabs.com 10.2.0.11
|
||||
# Expected: 10.2.0.20
|
||||
|
||||
# Ad blocking active
|
||||
nslookup doubleclick.net 10.2.0.11
|
||||
# Expected: 0.0.0.0
|
||||
|
||||
# Admin interface
|
||||
curl -s http://10.2.0.11/admin | grep -i pi-hole
|
||||
```
|
||||
|
||||
## Updates
|
||||
|
||||
```bash
|
||||
pihole -up
|
||||
```
|
||||
@@ -0,0 +1,143 @@
|
||||
# Servarr (Media VM) Setup
|
||||
|
||||
## Overview
|
||||
|
||||
The `servarr` VM runs the complete media stack: Plex and Jellyfin for streaming, the *arr suite for automated media management, and qBittorrent routed through Gluetun VPN for downloads. All services run via Docker Compose. The VM lives on Proxmox in VLAN 1020 (Homelab).
|
||||
|
||||
## VM Spec
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Hostname | servarr |
|
||||
| VLAN | 1020 (Homelab) |
|
||||
| Cores | 4 |
|
||||
| RAM | 8GB |
|
||||
| OS | Debian 12 |
|
||||
| Nesting | ✓ |
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Purpose |
|
||||
|---------|---------|
|
||||
| Plex | Media streaming (hardware transcoding) |
|
||||
| Jellyfin | Open-source media streaming alternative |
|
||||
| Sonarr | TV show management |
|
||||
| Radarr | Movie management |
|
||||
| Lidarr | Music management |
|
||||
| Prowlarr | Indexer aggregation (feeds Sonarr/Radarr/Lidarr) |
|
||||
| Bazarr | Subtitle management |
|
||||
| qBittorrent | Downloads — routed through Gluetun VPN container |
|
||||
| Calibre-Web Automated | Book library with auto-ingest |
|
||||
| Kavita | E-reader / comic reader |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- VM created in VLAN 1020
|
||||
- Gluetun-compatible VPN credentials (stored in Vaultwarden)
|
||||
- Media storage mounted (NFS or local disk)
|
||||
- Caddy routing configured for any public-facing services
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
apt update && apt upgrade -y
|
||||
apt install -y curl nano
|
||||
timedatectl set-timezone <your/timezone>
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
/opt/docker/servarr/
|
||||
├── docker-compose.yml
|
||||
├── .env
|
||||
└── config/
|
||||
├── plex/
|
||||
├── jellyfin/
|
||||
├── sonarr/
|
||||
├── radarr/
|
||||
├── lidarr/
|
||||
├── prowlarr/
|
||||
├── bazarr/
|
||||
├── qbittorrent/
|
||||
├── calibre/
|
||||
└── kavita/
|
||||
|
||||
/media/
|
||||
├── tv/
|
||||
├── movies/
|
||||
├── music/
|
||||
├── books/
|
||||
└── downloads/
|
||||
├── complete/
|
||||
└── incomplete/
|
||||
```
|
||||
|
||||
## qBittorrent + Gluetun (VPN-gated downloads)
|
||||
|
||||
qBittorrent runs inside the Gluetun network namespace. All download traffic exits through the VPN — no VPN = no download traffic.
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml excerpt
|
||||
services:
|
||||
gluetun:
|
||||
image: qmcgaw/gluetun:latest
|
||||
container_name: gluetun
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
environment:
|
||||
- VPN_SERVICE_PROVIDER=<provider>
|
||||
- VPN_TYPE=wireguard
|
||||
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
|
||||
- WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
|
||||
- SERVER_COUNTRIES=${SERVER_COUNTRIES}
|
||||
ports:
|
||||
- "8080:8080" # qBittorrent WebUI via Gluetun
|
||||
|
||||
qbittorrent:
|
||||
image: lscr.io/linuxserver/qbittorrent:latest
|
||||
container_name: qbittorrent
|
||||
network_mode: "service:gluetun" # all traffic through VPN
|
||||
environment:
|
||||
- WEBUI_PORT=8080
|
||||
volumes:
|
||||
- ./config/qbittorrent:/config
|
||||
- /media/downloads:/downloads
|
||||
```
|
||||
|
||||
## *arr Suite Configuration
|
||||
|
||||
Prowlarr is the central indexer — configure it first, then connect Sonarr/Radarr/Lidarr to it. All *arr services connect to qBittorrent as the download client (pointing to Gluetun's exposed port).
|
||||
|
||||
## Caddy Configuration
|
||||
|
||||
Add to Caddyfile on infra LXC for any *arr services you want accessible via HTTPS. Replace `<servarr-ip>` with the VM's IP.
|
||||
|
||||
```caddyfile
|
||||
# Example — Plex handles its own auth, no forward auth needed
|
||||
plex.lerkolabs.com {
|
||||
reverse_proxy <servarr-ip>:32400
|
||||
}
|
||||
|
||||
# *arr services — protect with Authentik forward auth
|
||||
sonarr.lerkolabs.com {
|
||||
import authentik_forward_auth
|
||||
reverse_proxy <servarr-ip>:8989
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# All containers running
|
||||
docker ps
|
||||
|
||||
# Gluetun VPN tunnel active
|
||||
docker exec gluetun wget -qO- https://api.ipinfo.io/ip
|
||||
# Should return VPN provider IP, not home WAN IP
|
||||
|
||||
# qBittorrent accessible
|
||||
curl -I http://localhost:8080
|
||||
```
|
||||
@@ -0,0 +1,157 @@
|
||||
# Vaultwarden Setup
|
||||
|
||||
## Overview
|
||||
|
||||
Vaultwarden runs in the `vault` LXC (10.2.0.X) in VLAN 1020 (Homelab). It is isolated — no shared containers, no shared Postgres. Accessible at `https://vault.lerkolabs.com` via Caddy with Authentik forward auth. VPN-only access (not exposed to internet directly).
|
||||
|
||||
## LXC Spec
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Hostname | vault |
|
||||
| IP | 10.2.0.X/24 (TBD) |
|
||||
| Gateway | 10.2.0.1 |
|
||||
| DNS | 10.2.0.11 |
|
||||
| Cores | 1 |
|
||||
| RAM | 256MB |
|
||||
| Disk | 4GB |
|
||||
| Template | debian-12-standard |
|
||||
| Nesting | ✓ |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Caddy running at 10.2.0.20
|
||||
- Pi-hole DNS record: `vault.lerkolabs.com → 10.2.0.20`
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
apt update && apt upgrade -y
|
||||
apt install -y curl nano
|
||||
timedatectl set-timezone <your/timezone>
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker
|
||||
mkdir -p /opt/docker/vaultwarden/data
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
# /opt/docker/vaultwarden/docker-compose.yml
|
||||
services:
|
||||
vaultwarden:
|
||||
image: vaultwarden/server:latest
|
||||
container_name: vaultwarden
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
environment:
|
||||
- DOMAIN=https://vault.lerkolabs.com
|
||||
- SIGNUPS_ALLOWED=true # set false after creating your account
|
||||
- WEBSOCKET_ENABLED=true
|
||||
- LOG_FILE=/data/vaultwarden.log
|
||||
- LOG_LEVEL=warn
|
||||
- ROCKET_PORT=80
|
||||
```
|
||||
|
||||
```bash
|
||||
cd /opt/docker/vaultwarden
|
||||
docker compose up -d
|
||||
docker logs -f vaultwarden
|
||||
```
|
||||
|
||||
## Initial Account Setup
|
||||
|
||||
1. Navigate to `https://vault.lerkolabs.com`
|
||||
2. Create your account
|
||||
3. Set `SIGNUPS_ALLOWED=false` in docker-compose.yml and restart:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Enable Admin Panel
|
||||
|
||||
```bash
|
||||
openssl rand -base64 48 # generate admin token
|
||||
```
|
||||
|
||||
Add to environment in docker-compose.yml:
|
||||
|
||||
```yaml
|
||||
- ADMIN_TOKEN=<generated_token>
|
||||
```
|
||||
|
||||
Access admin panel at: `https://vault.lerkolabs.com/admin`
|
||||
|
||||
## Caddy Configuration
|
||||
|
||||
Add to Caddyfile on infra LXC:
|
||||
|
||||
```caddyfile
|
||||
vault.lerkolabs.com {
|
||||
import authentik_forward_auth
|
||||
reverse_proxy 10.2.0.X:80
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "no-referrer"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Connecting Bitwarden Clients
|
||||
|
||||
In any official Bitwarden client (mobile, desktop, browser extension):
|
||||
|
||||
```
|
||||
Settings → Self-hosted Environment
|
||||
Server URL: https://vault.lerkolabs.com
|
||||
```
|
||||
|
||||
## Backup
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# /opt/backup-vaultwarden.sh
|
||||
BACKUP_DIR="/opt/backups/vaultwarden"
|
||||
DATE=$(date +%Y%m%d-%H%M%S)
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
docker stop vaultwarden
|
||||
tar -czf "$BACKUP_DIR/vaultwarden-$DATE.tar.gz" /opt/docker/vaultwarden/data/
|
||||
docker start vaultwarden
|
||||
|
||||
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +7 -delete
|
||||
```
|
||||
|
||||
```bash
|
||||
chmod +x /opt/backup-vaultwarden.sh
|
||||
crontab -e
|
||||
# Add: 0 2 * * * /opt/backup-vaultwarden.sh >> /var/log/vaultwarden-backup.log 2>&1
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# Container running
|
||||
docker ps
|
||||
|
||||
# Accessible via Caddy
|
||||
curl -I https://vault.lerkolabs.com
|
||||
# Expected: HTTP/2 200 or 302 (Authentik redirect)
|
||||
|
||||
# Data directory exists
|
||||
ls /opt/docker/vaultwarden/data/
|
||||
```
|
||||
|
||||
## Updates
|
||||
|
||||
```bash
|
||||
cd /opt/docker/vaultwarden
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
docker image prune -f
|
||||
```
|
||||
@@ -0,0 +1,130 @@
|
||||
# WireGuard Setup
|
||||
|
||||
## Overview
|
||||
|
||||
WireGuard VPN is configured directly in pfSense. It runs on UDP port 51820 — the only inbound port on the WAN interface. VPN clients get IPs in the 10.200.0.0/24 subnet and receive the same network access as LAN (Homelab + MGMT web GUI + Pi-hole DNS). No external software needed — pfSense handles it natively.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Listen Port | 51820 UDP |
|
||||
| VPN Subnet | 10.200.0.0/24 |
|
||||
| Access granted | Homelab (10.2.0.0/24) + MGMT web GUI + Pi-hole DNS |
|
||||
| Access blocked | Guest, IoT, WFH VLANs |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- pfSense running and accessible
|
||||
- WireGuard package installed (System → Package Manager → Available Packages → WireGuard)
|
||||
- Port 51820 UDP forwarded/open on WAN if behind NAT (not needed with IP Passthrough — pfSense has the public IP directly)
|
||||
- DDNS client configured on pfSense if WAN IP is dynamic
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Install WireGuard Package
|
||||
|
||||
Navigate to: **System → Package Manager → Available Packages**
|
||||
|
||||
Search "WireGuard" → Install.
|
||||
|
||||
### 2. Create WireGuard Tunnel
|
||||
|
||||
Navigate to: **VPN → WireGuard → Tunnels → Add Tunnel**
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| Enabled | ✓ |
|
||||
| Description | HomeVPN |
|
||||
| Listen Port | 51820 |
|
||||
| Interface Keys | Click "Generate" |
|
||||
| Interface Addresses | 10.200.0.1/24 |
|
||||
|
||||
Save. Note the **server public key** — you'll need it in peer configs.
|
||||
|
||||
### 3. Add Peers (Clients)
|
||||
|
||||
Navigate to: **VPN → WireGuard → Peers → Add Peer**
|
||||
|
||||
For each client device:
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| Tunnel | HomeVPN |
|
||||
| Description | e.g., iPhone |
|
||||
| Public Key | (generate on client, paste here) |
|
||||
| Allowed IPs | 10.200.0.X/32 (unique per peer) |
|
||||
|
||||
### 4. Create WireGuard Interface
|
||||
|
||||
Navigate to: **Interfaces → Assignments**
|
||||
|
||||
Assign the WireGuard tunnel as a new interface (e.g., `OPT1`). Rename it to `WG` or `VPN`.
|
||||
|
||||
Enable the interface: Interfaces → WG → Enable ✓
|
||||
|
||||
### 5. Firewall Rules
|
||||
|
||||
#### WAN — allow inbound WireGuard
|
||||
|
||||
Navigate to: **Firewall → Rules → WAN → Add**
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| Action | Pass |
|
||||
| Protocol | UDP |
|
||||
| Destination | WAN address |
|
||||
| Destination Port | 51820 |
|
||||
| Description | WireGuard VPN |
|
||||
|
||||
#### WG interface — allow VPN clients same access as LAN
|
||||
|
||||
Navigate to: **Firewall → Rules → WG → Add**
|
||||
|
||||
```
|
||||
Pass | IPv4 | Source: WG net | Destination: 10.2.0.0/24 | any | Homelab access
|
||||
Pass | IPv4 | Source: WG net | Destination: 10.0.0.0/24 | 443 | MGMT web GUI
|
||||
Pass | IPv4 | Source: WG net | Destination: 10.2.0.11 | 53 | Pi-hole DNS
|
||||
```
|
||||
|
||||
### 6. DNS for VPN Clients
|
||||
|
||||
In WireGuard peer config, set DNS to 10.2.0.11 (Pi-hole) so VPN clients get ad blocking and local name resolution.
|
||||
|
||||
## Client Configuration
|
||||
|
||||
Generate on each client device. Structure:
|
||||
|
||||
```ini
|
||||
[Interface]
|
||||
PrivateKey = <client private key>
|
||||
Address = 10.200.0.X/24
|
||||
DNS = 10.2.0.11
|
||||
|
||||
[Peer]
|
||||
PublicKey = <server public key from pfSense>
|
||||
Endpoint = <WAN IP or DDNS hostname>:51820
|
||||
AllowedIPs = 10.0.0.0/8 # route all RFC1918 through VPN, or use split tunnel
|
||||
PersistentKeepalive = 25
|
||||
```
|
||||
|
||||
In pfSense you can generate QR codes for mobile clients: VPN → WireGuard → Peers → (peer) → QR code icon.
|
||||
|
||||
## Key Rotation
|
||||
|
||||
When rotating keys or adding/removing peers:
|
||||
|
||||
1. Generate new key pair on client
|
||||
2. Update peer's public key in pfSense: VPN → WireGuard → Peers → Edit
|
||||
3. Update client config with new private key
|
||||
4. Apply changes in pfSense
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# From a mobile device on cellular (not home WiFi):
|
||||
# 1. Connect WireGuard
|
||||
# 2. curl https://outline.lerkolabs.com → should load with Authentik login
|
||||
# 3. curl http://10.2.0.11/admin → Pi-hole admin should be reachable
|
||||
|
||||
# On pfSense shell:
|
||||
wg show # should show peer with recent handshake
|
||||
```
|
||||
Reference in New Issue
Block a user