refactor: restructure docs, sync stacks with Caddyfile, harden compose templates

- Add compose stacks for apps already proxied by Caddy: bento-pdf,
  it-tools, paperless-ngx, and beszel-hub (with Makefile targets)
- Remove the unused Vault stack and the drawio Caddyfile block
- Fix Caddyfile gramps upstream to port 80 to match the compose file
- Bring immich and gramps-web up to the shared template pattern
  (no-new-privileges, log rotation, memory limits, healthchecks)
- Single-quote vaultwarden SSO_AUDIENCE_TRUSTED so regex values like
  ^\d{18}$ render as valid YAML
- Move root docs into kebab-case topic folders, rebuild the docs index
  to cover every doc, and fix all broken links
This commit is contained in:
Brian Pooe 2026-07-03 17:45:16 +02:00
parent f5a1c53713
commit 1815beadf4
29 changed files with 717 additions and 193 deletions

View file

@ -1,6 +1,6 @@
# Homelab Blueprint Makefile
.PHONY: help check-env status deploy-caddy deploy-ha deploy-arr deploy-immich deploy-dns deploy-vault deploy-postgres deploy-gramps deploy-beszel deploy-proxy deploy-youtarr
.PHONY: help check-env status deploy-caddy deploy-ha deploy-arr deploy-immich deploy-dns deploy-postgres deploy-gramps deploy-beszel deploy-beszel-hub deploy-proxy deploy-youtarr deploy-bento-pdf deploy-it-tools deploy-paperless
# Default shell
SHELL := /bin/bash
@ -21,12 +21,15 @@ help:
@echo " deploy-arr - Deploy Arr Stack (Media Management)"
@echo " deploy-immich - Deploy Immich (Photos)"
@echo " deploy-dns - Deploy Technitium DNS"
@echo " deploy-vault - Deploy HashiCorp Vault"
@echo " deploy-postgres - Deploy PostgreSQL"
@echo " deploy-gramps - Deploy Gramps Web"
@echo " deploy-beszel - Deploy Beszel Agent"
@echo " deploy-beszel-hub - Deploy Beszel Hub (dashboard)"
@echo " deploy-proxy - Deploy Beszel Socket Proxy"
@echo " deploy-youtarr - Deploy Youtarr"
@echo " deploy-bento-pdf - Deploy BentoPDF"
@echo " deploy-it-tools - Deploy IT-Tools"
@echo " deploy-paperless - Deploy Paperless-ngx"
@echo ""
@echo "Utility Targets:"
@echo " status - Show status of all deployed stacks"
@ -112,9 +115,6 @@ deploy-immich:
deploy-dns:
$(call deploy_stack,technitium,technitium)
deploy-vault:
$(call deploy_stack,vault,vault)
deploy-postgres:
$(call deploy_stack,postgres,postgres)
@ -129,6 +129,18 @@ deploy-gramps:
deploy-beszel:
$(call deploy_stack,beszel-agent,beszel)
deploy-beszel-hub:
$(call deploy_stack,beszel-hub,beszel-hub)
deploy-bento-pdf:
$(call deploy_stack,bento-pdf,bento-pdf)
deploy-it-tools:
$(call deploy_stack,it-tools,it-tools)
deploy-paperless:
$(call deploy_stack,paperless-ngx,paperless)
deploy-proxy:
$(call deploy_stack,socket-proxy,proxy)

View file

@ -23,16 +23,19 @@ Each stack guide in `docker-compose-files/<stack>/README.md` explains appdata pl
| Stack | Description |
|---|---|
| [Arr Stack](docker-compose-files/arr-stack/README.md) | Media management, downloads, VPN, Emby, and Recyclarr |
| [BentoPDF](docker-compose-files/bento-pdf/README.md) | Privacy-first client-side PDF toolkit |
| [Beszel Agent](docker-compose-files/beszel-agent/README.md) | Host and Docker monitoring agent |
| [Beszel Hub](docker-compose-files/beszel-hub/README.md) | Beszel monitoring dashboard |
| [Beszel Socket Proxy](docker-compose-files/socket-proxy/README.md) | Restricted Docker API access for Beszel |
| [Caddy](docker-compose-files/caddy/README.md) | Reverse proxy with Cloudflare DNS-01 |
| [Gramps Web](docker-compose-files/gramps-web/README.md) | Genealogy web application |
| [Home Assistant](docker-compose-files/homeassistant/README.md) | Home Assistant, Mosquitto, and Zigbee2MQTT |
| [Immich](docker-compose-files/immich/README.md) | Photo and video management with GPU acceleration |
| [IT-Tools](docker-compose-files/it-tools/README.md) | Developer and sysadmin utility toolbox |
| [Paperless-ngx](docker-compose-files/paperless-ngx/README.md) | Document management with OCR and full-text search |
| [PostgreSQL](docker-compose-files/postgres/README.md) | PostgreSQL and pgAdmin |
| [Technitium DNS](docker-compose-files/technitium/README.md) | DNS server and ad blocking |
| [Youtarr](docker-compose-files/youtarr/README.md) | YouTube downloader with metadata and media-server integrations |
| [Vault](docker-compose-files/vault/README.md) | HashiCorp Vault secrets management |
## Management
@ -81,9 +84,8 @@ Git.
## Operational Documentation
- [Documentation index](docs/README.md)
- [Arr Stack storage deployment](docs/DEPLOYMENT.md)
- [Proxmox NVMe storage layout](docs/proxmox/storage-layout.md)
- [Recyclarr](docs/RECYCLARR.md)
- [Troubleshooting](docs/TROUBLESHOOTING.md)
See the [documentation index](docs/README.md) for all guides. Most-used:
- [Arr Stack storage deployment](docs/arr-stack/deployment.md)
- [Troubleshooting](docs/troubleshooting.md)
- [UniFi custom firewall rules](docs/network/firewall-rules.md)

View file

@ -12,7 +12,7 @@ This stack is the exception to project-root `appdata/`:
- `DOCKERSTORAGEDIR` stores shared downloads and media.
The default paths are `/opt/arr-stack/appdata` and `/mnt/unas/arr_data`. Complete
[the storage deployment guide](../../docs/DEPLOYMENT.md) before starting.
[the storage deployment guide](../../docs/arr-stack/deployment.md) before starting.
## Recommended LXC Mode
@ -202,7 +202,7 @@ Use `12 GB` RAM and 6 CPU cores for multiple simultaneous Emby streams, large
libraries, heavy SABnzbd unpacking, or hundreds of active torrents. The `emby`
service is configured to use the `nvidia` runtime; ensure the Proxmox host
drivers and the NVIDIA Container Toolkit are installed and configured according
to the [NVIDIA GPU Passthrough guide](../../docs/nvidia/PROXMOX-NVIDIA-LXC.md).
to the [NVIDIA GPU Passthrough guide](../../docs/nvidia/proxmox-nvidia-lxc.md).
Without GPU passthrough, software transcoding can consume all available CPU;
avoid transcoding or allocate substantially more CPU.
@ -401,7 +401,7 @@ continues growing.
docker compose -f docker-compose.arr-stack.yml ps
```
See [Recyclarr Configuration Guide](../../docs/RECYCLARR.md) before changing
See [Recyclarr Configuration Guide](../../docs/arr-stack/recyclarr.md) before changing
the supplied quality profiles or custom-format scores.
## Apply Configuration Changes
@ -462,8 +462,8 @@ synchronized indexers have RSS enabled.
## Troubleshooting
Use [the deployment guide](../../docs/DEPLOYMENT.md) for storage and permissions
and [the troubleshooting guide](../../docs/TROUBLESHOOTING.md) for stack issues.
Use [the deployment guide](../../docs/arr-stack/deployment.md) for storage and permissions
and [the troubleshooting guide](../../docs/troubleshooting.md) for stack issues.
## Useful Commands

View file

@ -0,0 +1,5 @@
# BentoPDF
BENTOPDF_VERSION=latest
BENTOPDF_PORT=8080
BENTOPDF_BIND_IP=0.0.0.0
BENTOPDF_MEM_LIMIT=256m

View file

@ -0,0 +1,66 @@
# BentoPDF
Privacy-first PDF toolkit (merge, split, compress, convert, sign). All
processing happens client-side in the browser — the container only serves
static assets, so no document data ever touches the server.
## Persistent Data
None. The container is stateless; nothing needs backing up.
## Installation
1. Create `.env` if it does not exist:
```bash
cp -n .env.sample .env
```
2. Adjust the values in `.env` if needed:
```dotenv
TZ=Africa/Johannesburg
BENTOPDF_PORT=8080
BENTOPDF_MEM_LIMIT=256m
DOCKERLOGGING_MAXFILE=3
DOCKERLOGGING_MAXSIZE=10m
```
3. Deploy with `make`:
```bash
make deploy-bento-pdf
```
Manual equivalent:
```bash
./substitute_env.sh docker-compose-files/bento-pdf/template.yaml docker-compose.bento-pdf.yml .env
docker compose -f docker-compose.bento-pdf.yml config --quiet
docker compose -f docker-compose.bento-pdf.yml up -d
```
4. Verify the container and open `http://<host-ip>:8080`:
```bash
docker compose -f docker-compose.bento-pdf.yml ps
docker compose -f docker-compose.bento-pdf.yml logs --tail=100 bentopdf
```
## Reverse Proxy
The Caddyfile proxies `bento-pdf.<domain>` to this host on port 8080. After
changing the port, update `docker-compose-files/caddy/Caddyfile_template` and
run `make reload-caddy`.
## Useful Commands
```bash
# Tail logs
docker compose -f docker-compose.bento-pdf.yml logs -f bentopdf
# Recreate after an image update
docker compose -f docker-compose.bento-pdf.yml pull
docker compose -f docker-compose.bento-pdf.yml up -d
```

View file

@ -0,0 +1,32 @@
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:{{BENTOPDF_VERSION:-latest}}
container_name: bentopdf
restart: unless-stopped
environment:
TZ: "{{TZ}}"
volumes:
- /etc/localtime:/etc/localtime:ro
ports:
# All PDF processing happens client-side in the browser; the container
# only serves static assets on 8080.
- "{{BENTOPDF_BIND_IP:-0.0.0.0}}:{{BENTOPDF_PORT:-8080}}:8080"
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8080"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
deploy:
resources:
limits:
memory: "{{BENTOPDF_MEM_LIMIT:-256m}}"
reservations:
memory: 32M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"

View file

@ -0,0 +1,5 @@
# Beszel Hub
BESZEL_HUB_IMAGE_TAG=latest
BESZEL_HUB_PORT=8090
BESZEL_HUB_BIND_IP=0.0.0.0
BESZEL_HUB_MEM_LIMIT=256m

View file

@ -0,0 +1,85 @@
# Beszel Hub
Central web dashboard for Beszel server monitoring. Agents (see
[beszel-agent](../beszel-agent/README.md)) connect to this hub and report
host and Docker container metrics.
## Persistent Data
All hub state (PocketBase database, system records, alert config) is stored in
the Docker-managed named volume `beszel_hub_data`.
## Installation
1. Create `.env` if it does not exist:
```bash
cp -n .env.sample .env
```
2. Adjust the values in `.env` if needed:
```dotenv
TZ=Africa/Johannesburg
BESZEL_HUB_PORT=8090
BESZEL_HUB_MEM_LIMIT=256m
DOCKERLOGGING_MAXFILE=3
DOCKERLOGGING_MAXSIZE=10m
```
3. Deploy with `make`:
```bash
make deploy-beszel-hub
```
Manual equivalent:
```bash
./substitute_env.sh docker-compose-files/beszel-hub/template.yaml docker-compose.beszel-hub.yml .env
docker compose -f docker-compose.beszel-hub.yml config --quiet
docker compose -f docker-compose.beszel-hub.yml up -d
```
4. Open `http://<host-ip>:8090` and create the admin account.
## Connecting Agents
1. In the hub, click **Add System** and enter the agent host's IP and port
(default `45876`).
2. Copy the public key / token shown in the dialog into the agent's `.env`
(`BESZEL_AGENT_KEY`, `BESZEL_AGENT_TOKEN`, and
`BESZEL_AGENT_HUB_URL=http://<hub-ip>:8090`), then deploy the agent:
```bash
make deploy-beszel
```
## Reverse Proxy
The Caddyfile proxies `beszel.<domain>` to this host on port 8090. After
changing the port, update `docker-compose-files/caddy/Caddyfile_template` and
run `make reload-caddy`.
## Backup
Back up the `beszel_hub_data` named volume:
```bash
docker run --rm \
-v beszel_hub_data:/data:ro \
-v /opt/backups/beszel-hub:/backup \
busybox tar czf /backup/beszel-hub-$(date +%Y%m%d).tar.gz -C /data .
```
## Useful Commands
```bash
# Tail logs
docker compose -f docker-compose.beszel-hub.yml logs -f beszel-hub
# Recreate after an image update
docker compose -f docker-compose.beszel-hub.yml pull
docker compose -f docker-compose.beszel-hub.yml up -d
```

View file

@ -0,0 +1,35 @@
services:
beszel-hub:
image: henrygd/beszel:{{BESZEL_HUB_IMAGE_TAG:-latest}}
container_name: beszel-hub
restart: unless-stopped
ports:
- "{{BESZEL_HUB_BIND_IP:-0.0.0.0}}:{{BESZEL_HUB_PORT:-8090}}:8090"
environment:
TZ: "{{TZ}}"
volumes:
- /etc/localtime:/etc/localtime:ro
- beszel_hub_data:/beszel_data
healthcheck:
# Minimal image without a shell; the beszel binary probes /api/health.
test: ["CMD", "/beszel", "health", "--url", "http://localhost:8090"]
interval: 120s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
memory: "{{BESZEL_HUB_MEM_LIMIT:-256m}}"
reservations:
memory: 32M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
volumes:
beszel_hub_data:

View file

@ -123,7 +123,7 @@ unas.{{CADDY_BASE_DOMAIN:-home.example.com}} {
import office-admin-security
}
## LXC Apps (.6.19; next free: .12, .18, .19)
## LXC Apps (.6.19; next free: .9, .12, .18, .19)
bento-pdf.{{CADDY_BASE_DOMAIN:-home.example.com}} {
import common
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.6:8080
@ -134,12 +134,6 @@ it-tools.{{CADDY_BASE_DOMAIN:-home.example.com}} {
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.7:80
}
drawio.{{CADDY_BASE_DOMAIN:-home.example.com}} {
import common
redir / /draw/ 308
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.9:8080
}
paperless-ngx.{{CADDY_BASE_DOMAIN:-home.example.com}} {
import common
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.11:8000 {
@ -171,7 +165,7 @@ vaultwarden.{{CADDY_BASE_DOMAIN:-home.example.com}} {
gramps.{{CADDY_BASE_DOMAIN:-home.example.com}} {
import common
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.17:5000
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.17:80
}
## Arr Stack — LXC 101 (.20)

View file

@ -1,10 +1,13 @@
services:
grampsweb: &grampsweb
image: ghcr.io/gramps-project/grampsweb:latest
restart: always
restart: unless-stopped
ports:
- "80:5000"
networks:
- gramps_network
environment:
TZ: "{{TZ}}"
GRAMPSWEB_SECRET_KEY: "{{SECRET_KEY}}"
GRAMPSWEB_USER_DB_URI: "{{USER_DB_URI}}"
GRAMPSWEB_BASE_URL: "{{BASE_URL}}"
@ -14,6 +17,7 @@ services:
depends_on:
- grampsweb_redis
volumes:
- /etc/localtime:/etc/localtime:ro
- gramps_users:/app/users
- gramps_index:/app/indexdir
- gramps_thumb_cache:/app/thumbnail_cache
@ -25,6 +29,26 @@ services:
- gramps_data:/var/lib/gramps
tmpfs:
- /tmp
healthcheck:
test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:5000/')\" || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 512M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
grampsweb_celery:
!!merge <<: *grampsweb # YAML merge key copying the entire grampsweb service config
ports: []
@ -33,10 +57,42 @@ services:
- grampsweb
- grampsweb_redis
command: celery -A gramps_webapi.celery worker --loglevel=INFO --concurrency=2
# The web healthcheck inherited from the anchor does not apply to a worker.
healthcheck:
disable: true
grampsweb_redis:
image: docker.io/library/redis:7.2.4-alpine
container_name: grampsweb_redis
restart: always
restart: unless-stopped
networks:
- gramps_network
volumes:
- /etc/localtime:/etc/localtime:ro
healthcheck:
test: ["CMD-SHELL", "redis-cli ping || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 32M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
networks:
gramps_network:
driver: bridge
name: gramps_network
volumes:
gramps_users:

View file

@ -52,6 +52,13 @@ services:
memory: 4G
reservations:
memory: 1G
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
immich-machine-learning:
container_name: immich_machine_learning
@ -76,13 +83,35 @@ services:
- driver: nvidia
count: 1
capabilities: [compute, utility]
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9
restart: always
environment:
- TZ={{TZ}}
healthcheck:
test: ["CMD-SHELL", "redis-cli ping || exit 1"]
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 32M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
database:
container_name: immich_postgres
@ -92,6 +121,7 @@ services:
- POSTGRES_USER={{IMMICH_DB_USERNAME}}
- POSTGRES_DB={{IMMICH_DB_DATABASE_NAME}}
- POSTGRES_INITDB_ARGS=--data-checksums
- TZ={{TZ}}
volumes:
- {{IMMICH_DATA_DIR}}/postgres:/var/lib/postgresql/data
restart: always
@ -100,3 +130,12 @@ services:
interval: 10s
timeout: 5s
retries: 5
# No memory limit: vector index builds are spiky and an OOM kill here
# risks database corruption.
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"

View file

@ -0,0 +1,5 @@
# IT-Tools
IT_TOOLS_VERSION=latest
IT_TOOLS_PORT=80
IT_TOOLS_BIND_IP=0.0.0.0
IT_TOOLS_MEM_LIMIT=128m

View file

@ -0,0 +1,66 @@
# IT-Tools
Collection of handy developer and sysadmin tools (encoders, converters, token
generators, network calculators) served as a static site. Everything runs
client-side in the browser.
## Persistent Data
None. The container is stateless; nothing needs backing up.
## Installation
1. Create `.env` if it does not exist:
```bash
cp -n .env.sample .env
```
2. Adjust the values in `.env` if needed:
```dotenv
TZ=Africa/Johannesburg
IT_TOOLS_PORT=80
IT_TOOLS_MEM_LIMIT=128m
DOCKERLOGGING_MAXFILE=3
DOCKERLOGGING_MAXSIZE=10m
```
3. Deploy with `make`:
```bash
make deploy-it-tools
```
Manual equivalent:
```bash
./substitute_env.sh docker-compose-files/it-tools/template.yaml docker-compose.it-tools.yml .env
docker compose -f docker-compose.it-tools.yml config --quiet
docker compose -f docker-compose.it-tools.yml up -d
```
4. Verify the container and open `http://<host-ip>`:
```bash
docker compose -f docker-compose.it-tools.yml ps
docker compose -f docker-compose.it-tools.yml logs --tail=100 it-tools
```
## Reverse Proxy
The Caddyfile proxies `it-tools.<domain>` to this host on port 80. After
changing the port, update `docker-compose-files/caddy/Caddyfile_template` and
run `make reload-caddy`.
## Useful Commands
```bash
# Tail logs
docker compose -f docker-compose.it-tools.yml logs -f it-tools
# Recreate after an image update
docker compose -f docker-compose.it-tools.yml pull
docker compose -f docker-compose.it-tools.yml up -d
```

View file

@ -0,0 +1,31 @@
services:
it-tools:
image: ghcr.io/corentinth/it-tools:{{IT_TOOLS_VERSION:-latest}}
container_name: it-tools
restart: unless-stopped
environment:
TZ: "{{TZ}}"
volumes:
- /etc/localtime:/etc/localtime:ro
ports:
# Static nginx site; everything runs client-side in the browser.
- "{{IT_TOOLS_BIND_IP:-0.0.0.0}}:{{IT_TOOLS_PORT:-80}}:80"
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:80"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
deploy:
resources:
limits:
memory: "{{IT_TOOLS_MEM_LIMIT:-128m}}"
reservations:
memory: 16M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"

View file

@ -0,0 +1,21 @@
# Paperless-ngx
PAPERLESS_VERSION=latest
PAPERLESS_PORT=8000
PAPERLESS_BIND_IP=0.0.0.0
PAPERLESS_MEM_LIMIT=2G
# Full URL where Paperless is reachable (needed for CSRF behind the proxy)
PAPERLESS_URL=https://paperless-ngx.home.example.com
# Generate with: openssl rand -base64 48
PAPERLESS_SECRET_KEY=<random-secret>
# Three-letter OCR language code(s), e.g. eng or eng+deu
PAPERLESS_OCR_LANGUAGE=eng
# Host directory watched for new documents to ingest
PAPERLESS_CONSUME_DIR=/opt/appdata/paperless/consume
# Optional: superuser created on first start (leave blank to create manually)
PAPERLESS_ADMIN_USER=
PAPERLESS_ADMIN_PASSWORD=

View file

@ -0,0 +1,102 @@
# Paperless-ngx
Document management system that scans, indexes, and archives paper documents
with full-text OCR search. Runs with the bundled SQLite database and a Redis
task broker.
## Persistent Data
- `paperless_data` named volume — database, search index, classification model.
- `paperless_media` named volume — original and archived documents.
- `paperless_export` named volume — target for `document_exporter` backups.
- `PAPERLESS_CONSUME_DIR` host directory — watched for new documents; safe to
empty once documents are ingested.
## Installation
1. Create `.env` if it does not exist:
```bash
cp -n .env.sample .env
```
2. Set the required values in `.env`:
```dotenv
TZ=Africa/Johannesburg
PAPERLESS_URL=https://paperless-ngx.home.example.com
PAPERLESS_SECRET_KEY=<random-secret> # openssl rand -base64 48
PAPERLESS_CONSUME_DIR=/opt/appdata/paperless/consume
PUID=1000
PGID=1000
DOCKERLOGGING_MAXFILE=3
DOCKERLOGGING_MAXSIZE=10m
```
`PAPERLESS_URL` must match the URL used in the browser, otherwise CSRF
checks fail behind the reverse proxy.
3. Create the consume directory:
```bash
mkdir -p /opt/appdata/paperless/consume
chown 1000:1000 /opt/appdata/paperless/consume
```
4. Deploy with `make`:
```bash
make deploy-paperless
```
Manual equivalent:
```bash
./substitute_env.sh docker-compose-files/paperless-ngx/template.yaml docker-compose.paperless.yml .env
docker compose -f docker-compose.paperless.yml config --quiet
docker compose -f docker-compose.paperless.yml up -d
```
5. Create the first user, unless `PAPERLESS_ADMIN_USER` is set in `.env`:
```bash
docker exec -it paperless createsuperuser
```
6. Verify the containers and open `http://<host-ip>:8000`:
```bash
docker compose -f docker-compose.paperless.yml ps
docker compose -f docker-compose.paperless.yml logs --tail=100 paperless
```
## Reverse Proxy
The Caddyfile proxies `paperless-ngx.<domain>` to this host on port 8000 with
`header_up Host {host}` so CSRF origin checks pass. After changing the port,
update `docker-compose-files/caddy/Caddyfile_template` and run
`make reload-caddy`.
## Backup
Run the built-in exporter, then back up the `paperless_export` volume (it
contains documents plus a manifest that restores tags, correspondents, and
metadata):
```bash
docker exec paperless document_exporter /usr/src/paperless/export --zip
```
## Useful Commands
```bash
# Tail logs
docker compose -f docker-compose.paperless.yml logs -f paperless
# Retrain the document classifier
docker exec paperless document_create_classifier
# Re-run OCR on a document
docker exec paperless document_archiver --overwrite --document <id>
```

View file

@ -0,0 +1,93 @@
services:
paperless-redis:
image: docker.io/library/redis:7-alpine
container_name: paperless-redis
restart: unless-stopped
networks:
- paperless_network
volumes:
- /etc/localtime:/etc/localtime:ro
- paperless_redis_data:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli ping || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 32M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
paperless:
image: ghcr.io/paperless-ngx/paperless-ngx:{{PAPERLESS_VERSION:-latest}}
container_name: paperless
restart: unless-stopped
networks:
- paperless_network
ports:
- "{{PAPERLESS_BIND_IP:-0.0.0.0}}:{{PAPERLESS_PORT:-8000}}:8000"
environment:
PAPERLESS_REDIS: redis://paperless-redis:6379
# Uses the bundled SQLite database; set PAPERLESS_DBHOST here to switch
# to PostgreSQL later (requires a data export/import migration).
PAPERLESS_URL: "{{PAPERLESS_URL}}"
PAPERLESS_SECRET_KEY: "{{PAPERLESS_SECRET_KEY}}"
PAPERLESS_TIME_ZONE: "{{TZ}}"
PAPERLESS_OCR_LANGUAGE: "{{PAPERLESS_OCR_LANGUAGE:-eng}}"
USERMAP_UID: "{{PUID}}"
USERMAP_GID: "{{PGID}}"
# Optional: create this superuser on first start (leave blank to run
# `createsuperuser` manually instead).
PAPERLESS_ADMIN_USER: "{{PAPERLESS_ADMIN_USER:-}}"
PAPERLESS_ADMIN_PASSWORD: "{{PAPERLESS_ADMIN_PASSWORD:-}}"
volumes:
- /etc/localtime:/etc/localtime:ro
- paperless_data:/usr/src/paperless/data
- paperless_media:/usr/src/paperless/media
- paperless_export:/usr/src/paperless/export
# Drop documents here to have them ingested automatically.
- {{PAPERLESS_CONSUME_DIR}}:/usr/src/paperless/consume
depends_on:
paperless-redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -fs -S -L http://localhost:8000 || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
deploy:
resources:
limits:
# OCR of large scans is memory-hungry.
memory: "{{PAPERLESS_MEM_LIMIT:-2G}}"
reservations:
memory: 512M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
networks:
paperless_network:
driver: bridge
name: paperless_network
volumes:
paperless_redis_data:
paperless_data:
paperless_media:
paperless_export:

View file

@ -105,7 +105,7 @@ Avoid `tif-onlydomains.txt` with the default 512 MB limit because it can cause
## Enable On UDM Pro SE
Use this sequence with the latest UniFi OS and UniFi Network. It follows the
current [zone migration design](../../docs/network/udm-pro-zones-migration.md):
current zone design (see [firewall-rules.md](../../docs/network/firewall-rules.md)):
- Technitium: `10.0.10.5`, Office VLAN 10, `TRUSTED` zone
- Caddy: `10.0.15.5`, DMZ VLAN 15, `DMZ` zone
@ -228,16 +228,12 @@ Create these only while migrating each VLAN into its target custom zone:
Place each DNS allow above that zone's RFC1918 and terminal block policies.
Remove the matching temporary rule after the final rule works.
For the exact field-by-field policy form, use
[How the policy tables map to the Create Policy panel](../../docs/network/udm-pro-zones-migration.md#how-the-policy-tables-map-to-the-create-policy-panel).
### Later: Enable DNS Enforcement
UniFi Network 10.4.57 does not expose supported LAN-facing DNS DNAT. After the
final zone policies and DHCP-provided DNS work, follow
[DNS Handling - DNAT Redirect Strategy](../../docs/network/udm-pro-zones-migration.md#8-dns-handling--dnat-redirect-strategy)
to verify that each restricted zone can query Technitium and that port 53
queries to other destinations are blocked.
final zone policies and DHCP-provided DNS work, verify that each restricted
zone can query Technitium and that port 53 queries to other destinations are
blocked.
Do not block public DNS from `TRUSTED` or `DMZ`. Office clients are trusted,
and Caddy in DMZ must query public resolvers for ACME DNS-01.

View file

@ -1,91 +0,0 @@
# Vault
Docker deployment for HashiCorp Vault. Run all commands from the repository
root.
## Persistent Data
Vault data and logs are stored in the Docker-managed named volumes
`vault_data` and `vault_logs`. Host-managed configuration and TLS material
remain read-only mounts from `appdata/vault/config` and `appdata/vault/tls`.
Vault expects its configuration at `appdata/vault/config/vault.hcl`.
Existing deployments that store data and logs under `appdata/vault` must copy
them into the named volumes before recreating the container.
## Installation
1. Create `.env` if it does not exist:
```bash
cp -n .env.sample .env
```
2. Set the shared values in `.env`:
```dotenv
TZ=Africa/Johannesburg
DOCKERLOGGING_MAXFILE=3
DOCKERLOGGING_MAXSIZE=10m
```
3. Create appdata, provide a production-appropriate `vault.hcl`, and render the
template:
```bash
mkdir -p appdata/vault/{config,tls}
./substitute_env.sh docker-compose-files/vault/template.yaml docker-compose.vault.yml .env
```
4. Validate and start Vault:
```bash
test -f appdata/vault/config/vault.hcl
docker compose -f docker-compose.vault.yml config --quiet
docker compose -f docker-compose.vault.yml up -d
```
5. Verify the container:
```bash
docker compose -f docker-compose.vault.yml ps
docker compose -f docker-compose.vault.yml logs --tail=100 vault
```
## Apply Configuration Changes
```bash
./substitute_env.sh docker-compose-files/vault/template.yaml docker-compose.vault.yml .env
docker compose -f docker-compose.vault.yml up -d
```
Restart Vault after changing `appdata/vault/config/vault.hcl`.
## Initial Setup
Initialize and unseal Vault according to your chosen storage and seal
configuration. Store recovery material outside this repository.
## Network Requirements
- Vault listens on ports `8200` and `8201`.
- Restrict both ports to trusted networks.
- Configure TLS in `vault.hcl`; the container expects HTTPS.
## Troubleshooting
Check `vault.hcl`, TLS files, and container logs if Vault does not become
healthy. The host-mounted config and TLS files must be readable by the
container, but private keys should not be made world-readable.
## Useful Commands
```bash
docker compose -f docker-compose.vault.yml exec vault vault status
```
## Backup
Back up the `vault_data` and `vault_logs` named volumes plus
`appdata/vault/config` and `appdata/vault/tls`, according to the storage
backend configured in `vault.hcl`.

View file

@ -1,52 +0,0 @@
services:
vault:
image: hashicorp/vault:1.19.2
container_name: vault
restart: unless-stopped
ports:
- "8200:8200"
- "8201:8201"
environment:
TZ: {{TZ}}
VAULT_ADDR: https://127.0.0.1:8200
VAULT_LOG_LEVEL: info
VAULT_SKIP_VERIFY: "false"
volumes:
- /etc/localtime:/etc/localtime:ro
- ./appdata/vault/config:/vault/config:ro
- ./appdata/vault/tls:/vault/tls:ro
- vault_data:/vault/data
- vault_logs:/vault/logs
cap_add:
- IPC_LOCK
command: vault server -config=/vault/config/vault.hcl
networks:
- vault_network
healthcheck:
test: ["CMD-SHELL", "wget --no-check-certificate --no-verbose --tries=1 --spider https://localhost:8200/v1/sys/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 128M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
networks:
vault_network:
driver: bridge
name: vault_network
volumes:
vault_data:
vault_logs:

View file

@ -28,8 +28,9 @@ services:
SSO_CLIENT_SECRET: "{{VAULTWARDEN_SSO_CLIENT_SECRET:-}}"
SSO_SCOPES: "{{VAULTWARDEN_SSO_SCOPES:-openid email profile offline_access}}"
SSO_PKCE: "{{VAULTWARDEN_SSO_PKCE:-true}}"
# Zitadel puts all project app client IDs in aud; trust any 18-digit ID
SSO_AUDIENCE_TRUSTED: "{{VAULTWARDEN_SSO_AUDIENCE_TRUSTED:-}}"
# Zitadel puts all project app client IDs in aud; trust any 18-digit ID.
# Single-quoted: the value is a regex and \d is not a valid YAML escape.
SSO_AUDIENCE_TRUSTED: '{{VAULTWARDEN_SSO_AUDIENCE_TRUSTED:-}}'
volumes:
- /etc/localtime:/etc/localtime:ro
- vaultwarden_data:/data

View file

@ -3,16 +3,20 @@
services:
youtarr-db:
# Pinned to 10.3: upstream Youtarr relies on --innodb-large-prefix, which
# was removed in MariaDB 10.4.
image: mariadb:10.3
container_name: youtarr-db
restart: unless-stopped
environment:
TZ: {{TZ}}
MYSQL_ROOT_PASSWORD: {{YOUTARR_DB_ROOT_PASSWORD}}
MYSQL_DATABASE: youtarr
MYSQL_TCP_PORT: 3321
MYSQL_CHARSET: utf8mb4
MYSQL_COLLATION: utf8mb4_unicode_ci
volumes:
- /etc/localtime:/etc/localtime:ro
- youtarr_db_data:/var/lib/mysql
command:
- --port=3321

View file

@ -1,17 +1,34 @@
# Documentation
Current operational documentation:
Operational documentation, grouped by topic. For per-stack installation
guides, see the [Stacks table in the root README](../README.md#stacks).
- [Arr-stack deployment](DEPLOYMENT.md): UNAS NFS, Proxmox LXC, permissions, and hardlinks.
- [NVIDIA GPU Passthrough](nvidia/PROXMOX-NVIDIA-LXC.md): Host -> LXC -> Docker for hardware transcoding.
- [Recyclarr](RECYCLARR.md): Recyclarr configuration and sync workflow.
- [Troubleshooting](TROUBLESHOOTING.md): Common stack issues and recovery steps.
- [Proxmox NVMe storage layout](proxmox/storage-layout.md): Fresh-install named storage layout for OS files and VM/LXC disks.
- [U-NAS in-pool share move](unas/in-pool-reflink-move.md): Move large folders between UniFi Drive shares using Btrfs reflinks without duplicating the data.
## Arr Stack
- [Deployment](arr-stack/deployment.md): UNAS NFS, Proxmox LXC, permissions, and hardlinks.
- [Recyclarr](arr-stack/recyclarr.md): Recyclarr configuration and sync workflow.
## Proxmox
- [NVMe storage layout](proxmox/storage-layout.md): Fresh-install named storage layout for OS files and VM/LXC disks.
- [LXC boot dependencies](proxmox/nfs-lxc-boot-dependency.md): Making NFS mounts and NVIDIA device nodes ready before LXCs start.
- [NVIDIA GPU passthrough](nvidia/proxmox-nvidia-lxc.md): Host -> LXC -> Docker for hardware transcoding.
## Network
- [Firewall rules](network/firewall-rules.md): Custom UniFi firewall rules, target IPs, ports, and stateful return traffic.
- [Caddy firewall policies](network/allow-caddy-to-apps.md): Zone policies that allow the Caddy DMZ host to reach apps on other VLANs.
- [Rack layout](network/rack-layout.md): Current UNAS and MikroTik rack layout.
- [Stack installation guides](../README.md#stacks): Links to every Docker stack README.
- [Technitium deployment](../docker-compose-files/technitium/README.md): DNS and ad-blocking service setup.
- [Caddy deployment](../docker-compose-files/caddy/README.md): Reverse proxy with Cloudflare DNS-01.
- [Immich deployment](../docker-compose-files/immich/README.md): Self-hosted photo management with GPU acceleration.
- [Youtarr deployment](../docker-compose-files/youtarr/README.md): Self-hosted YouTube downloader with metadata generation.
- [UniFi custom firewall rules](network/firewall-rules.md): Reference list of custom firewall rules, target IPs, ports, and stateful return traffic configurations.
## UNAS Storage
- [NFS and SMB mounts](unas/nfs-mounts.md): Persistent UniFi Drive share mounts on Proxmox and LXC bind mounts.
- [In-pool share move](unas/in-pool-reflink-move.md): Move large folders between UniFi Drive shares using Btrfs reflinks without duplicating data.
## Immich
- [Mobile sync](immich/mobile-sync.md): Phone -> NAS -> external library sync that avoids re-upload after server-side deletes.
## Troubleshooting
- [Troubleshooting guide](troubleshooting.md): Common stack issues and recovery steps.

View file

@ -1,7 +1,7 @@
# Caddy Firewall Policies — Missing and Incorrect Rules
> [!NOTE]
> For the complete list of custom firewall rules, see [firewall-rules.md](file:///home/brian/Documents/homelab-blueprint/docs/network/firewall-rules.md).
> For the complete list of custom firewall rules, see [firewall-rules.md](firewall-rules.md).
Audit findings from the zone policy review. Apply these in UniFi
**Settings → Policy Engine → Policy Table** in the order shown. All