diff --git a/Makefile b/Makefile index 36a0bc0..7030859 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/README.md b/README.md index 729b8a2..15b990d 100644 --- a/README.md +++ b/README.md @@ -23,16 +23,19 @@ Each stack guide in `docker-compose-files//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) diff --git a/docker-compose-files/arr-stack/README.md b/docker-compose-files/arr-stack/README.md index 69b97e2..f5a72f0 100644 --- a/docker-compose-files/arr-stack/README.md +++ b/docker-compose-files/arr-stack/README.md @@ -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 diff --git a/docker-compose-files/bento-pdf/.env.sample b/docker-compose-files/bento-pdf/.env.sample new file mode 100644 index 0000000..dda3036 --- /dev/null +++ b/docker-compose-files/bento-pdf/.env.sample @@ -0,0 +1,5 @@ +# BentoPDF +BENTOPDF_VERSION=latest +BENTOPDF_PORT=8080 +BENTOPDF_BIND_IP=0.0.0.0 +BENTOPDF_MEM_LIMIT=256m diff --git a/docker-compose-files/bento-pdf/README.md b/docker-compose-files/bento-pdf/README.md new file mode 100644 index 0000000..9cc43f8 --- /dev/null +++ b/docker-compose-files/bento-pdf/README.md @@ -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://: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.` 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 +``` diff --git a/docker-compose-files/bento-pdf/template.yaml b/docker-compose-files/bento-pdf/template.yaml new file mode 100644 index 0000000..019985b --- /dev/null +++ b/docker-compose-files/bento-pdf/template.yaml @@ -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}}" diff --git a/docker-compose-files/beszel-hub/.env.sample b/docker-compose-files/beszel-hub/.env.sample new file mode 100644 index 0000000..2cb46c2 --- /dev/null +++ b/docker-compose-files/beszel-hub/.env.sample @@ -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 diff --git a/docker-compose-files/beszel-hub/README.md b/docker-compose-files/beszel-hub/README.md new file mode 100644 index 0000000..ae9d20e --- /dev/null +++ b/docker-compose-files/beszel-hub/README.md @@ -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://: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://:8090`), then deploy the agent: + + ```bash + make deploy-beszel + ``` + +## Reverse Proxy + +The Caddyfile proxies `beszel.` 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 +``` diff --git a/docker-compose-files/beszel-hub/template.yaml b/docker-compose-files/beszel-hub/template.yaml new file mode 100644 index 0000000..5d3ce16 --- /dev/null +++ b/docker-compose-files/beszel-hub/template.yaml @@ -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: diff --git a/docker-compose-files/caddy/Caddyfile_template b/docker-compose-files/caddy/Caddyfile_template index af0ff37..8f2b8a6 100644 --- a/docker-compose-files/caddy/Caddyfile_template +++ b/docker-compose-files/caddy/Caddyfile_template @@ -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) diff --git a/docker-compose-files/gramps-web/template.yaml b/docker-compose-files/gramps-web/template.yaml index ee2c555..f1826a1 100644 --- a/docker-compose-files/gramps-web/template.yaml +++ b/docker-compose-files/gramps-web/template.yaml @@ -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: diff --git a/docker-compose-files/immich/template.yaml b/docker-compose-files/immich/template.yaml index 2aa1cdc..e2ab489 100644 --- a/docker-compose-files/immich/template.yaml +++ b/docker-compose-files/immich/template.yaml @@ -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}}" diff --git a/docker-compose-files/it-tools/.env.sample b/docker-compose-files/it-tools/.env.sample new file mode 100644 index 0000000..ce9760a --- /dev/null +++ b/docker-compose-files/it-tools/.env.sample @@ -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 diff --git a/docker-compose-files/it-tools/README.md b/docker-compose-files/it-tools/README.md new file mode 100644 index 0000000..54df955 --- /dev/null +++ b/docker-compose-files/it-tools/README.md @@ -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://`: + + ```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.` 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 +``` diff --git a/docker-compose-files/it-tools/template.yaml b/docker-compose-files/it-tools/template.yaml new file mode 100644 index 0000000..98f80b9 --- /dev/null +++ b/docker-compose-files/it-tools/template.yaml @@ -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}}" diff --git a/docker-compose-files/paperless-ngx/.env.sample b/docker-compose-files/paperless-ngx/.env.sample new file mode 100644 index 0000000..987be6e --- /dev/null +++ b/docker-compose-files/paperless-ngx/.env.sample @@ -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= + +# 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= diff --git a/docker-compose-files/paperless-ngx/README.md b/docker-compose-files/paperless-ngx/README.md new file mode 100644 index 0000000..a964904 --- /dev/null +++ b/docker-compose-files/paperless-ngx/README.md @@ -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= # 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://: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.` 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 +``` diff --git a/docker-compose-files/paperless-ngx/template.yaml b/docker-compose-files/paperless-ngx/template.yaml new file mode 100644 index 0000000..5df388b --- /dev/null +++ b/docker-compose-files/paperless-ngx/template.yaml @@ -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: diff --git a/docker-compose-files/technitium/README.md b/docker-compose-files/technitium/README.md index df3b855..dd2f851 100644 --- a/docker-compose-files/technitium/README.md +++ b/docker-compose-files/technitium/README.md @@ -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. diff --git a/docker-compose-files/vault/README.md b/docker-compose-files/vault/README.md deleted file mode 100644 index 54b4e56..0000000 --- a/docker-compose-files/vault/README.md +++ /dev/null @@ -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`. diff --git a/docker-compose-files/vault/template.yaml b/docker-compose-files/vault/template.yaml deleted file mode 100644 index 4e4f670..0000000 --- a/docker-compose-files/vault/template.yaml +++ /dev/null @@ -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: diff --git a/docker-compose-files/vaultwarden/template.yaml b/docker-compose-files/vaultwarden/template.yaml index fa03f04..f3bd8dd 100644 --- a/docker-compose-files/vaultwarden/template.yaml +++ b/docker-compose-files/vaultwarden/template.yaml @@ -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 diff --git a/docker-compose-files/youtarr/template.yaml b/docker-compose-files/youtarr/template.yaml index 8794ccc..ab0b5d2 100644 --- a/docker-compose-files/youtarr/template.yaml +++ b/docker-compose-files/youtarr/template.yaml @@ -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 diff --git a/docs/README.md b/docs/README.md index 264bb34..7ddd24a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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. diff --git a/docs/DEPLOYMENT.md b/docs/arr-stack/deployment.md similarity index 100% rename from docs/DEPLOYMENT.md rename to docs/arr-stack/deployment.md diff --git a/docs/RECYCLARR.md b/docs/arr-stack/recyclarr.md similarity index 100% rename from docs/RECYCLARR.md rename to docs/arr-stack/recyclarr.md diff --git a/docs/network/allow-caddy-to-apps.md b/docs/network/allow-caddy-to-apps.md index 1654706..421b181 100644 --- a/docs/network/allow-caddy-to-apps.md +++ b/docs/network/allow-caddy-to-apps.md @@ -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 diff --git a/docs/nvidia/PROXMOX-NVIDIA-LXC.md b/docs/nvidia/proxmox-nvidia-lxc.md similarity index 100% rename from docs/nvidia/PROXMOX-NVIDIA-LXC.md rename to docs/nvidia/proxmox-nvidia-lxc.md diff --git a/docs/TROUBLESHOOTING.md b/docs/troubleshooting.md similarity index 100% rename from docs/TROUBLESHOOTING.md rename to docs/troubleshooting.md