The firewall script deleted its rules before re-inserting them, and Docker restarted the stack on its own (restart: unless-stopped) with no ordering against the firewall unit, so port 8090 could be open to the LAN while the rules were reapplied. - mealquest-firewall.sh fails closed: if the chain is not already exactly right, it inserts the DROP at position 1 first, deletes its other tagged rules by number, then inserts the two RETURN rules above it. A correct chain is left untouched. - New mealquest.service owns the compose stack: Requires= and After= mealquest-firewall.service, PartOf=docker.service. - compose.yaml: restart: "no" on both services, so Docker never starts them before the firewall. - RUNBOOK: deploy, rollback, restore and key rotation start and stop the stack with systemctl; documents the stack unit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
3.1 KiB
YAML
101 lines
3.1 KiB
YAML
# Production stack for MealQuest on 10.60.1.14 (D-018). Build on the host,
|
|
# then start through systemd:
|
|
#
|
|
# docker compose -f deploy/compose.yaml build
|
|
# sudo systemctl restart mealquest
|
|
#
|
|
# The services use restart: "no" on purpose: Docker must not start them
|
|
# before the DOCKER-USER firewall is applied. deploy/systemd/mealquest.service
|
|
# owns start and stop and requires mealquest-firewall.service first.
|
|
#
|
|
# Configuration comes from deploy/.env (copy deploy/.env.example) and secrets
|
|
# from deploy/secrets/; neither is committed. Compose bind-mounts file secrets
|
|
# with their host owner and mode, so each file in deploy/secrets/ must be
|
|
# owned by the container user 10001:10001 with mode 0600.
|
|
name: mealquest
|
|
|
|
services:
|
|
mealquest:
|
|
build:
|
|
context: ..
|
|
dockerfile: Dockerfile
|
|
user: "10001:10001"
|
|
restart: "no"
|
|
env_file:
|
|
- path: .env
|
|
required: true
|
|
# Fixed by this file's container shape (listen port, volume, secret
|
|
# mounts); deploy/.env must not set them.
|
|
environment:
|
|
MEALQUEST_HTTP_ADDR: ":8080"
|
|
MEALQUEST_DATA_DIR: /data
|
|
MEALQUEST_SECRET_KEY_FILE: /run/secrets/secret_key
|
|
MEALQUEST_OIDC_CLIENT_SECRET_FILE: /run/secrets/oidc_client_secret
|
|
secrets:
|
|
- secret_key
|
|
- oidc_client_secret
|
|
volumes:
|
|
- mealquest-data:/data
|
|
ports:
|
|
- "${MEALQUEST_BIND_ADDRESS:?set MEALQUEST_BIND_ADDRESS in deploy/.env}:8090:8080"
|
|
networks:
|
|
- mealquest
|
|
healthcheck:
|
|
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/health"]
|
|
interval: 30s
|
|
timeout: 3s
|
|
start_period: 10s
|
|
retries: 3
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
cap_drop:
|
|
- ALL
|
|
|
|
# Litestream streams the database to the Synology NAS over SFTP
|
|
# (config: deploy/litestream.yml). The SFTP private key
|
|
# deploy/secrets/litestream_key is a dedicated keypair, owned by
|
|
# 10001:10001 with mode 0600 like the other secrets.
|
|
#
|
|
# The data volume is mounted read-write on purpose: Litestream writes its
|
|
# tracking tables into the database and checkpoints the WAL. It runs as
|
|
# the app's own user (owner of the volume) with a read-only root
|
|
# filesystem, no privilege escalation and no Linux capabilities.
|
|
#
|
|
# Restore: follow "Restore the database" in deploy/RUNBOOK.md (stop the
|
|
# stack, move the database with its -wal and -shm aside, restore, check).
|
|
litestream:
|
|
image: litestream/litestream:0.5.17
|
|
user: "10001:10001"
|
|
restart: "no"
|
|
command: ["replicate", "-config", "/etc/litestream.yml"]
|
|
depends_on:
|
|
mealquest:
|
|
condition: service_healthy
|
|
volumes:
|
|
- mealquest-data:/data
|
|
- ./litestream.yml:/etc/litestream.yml:ro
|
|
- ./secrets/litestream_key:/etc/litestream/sftp_key:ro
|
|
networks:
|
|
- mealquest
|
|
read_only: true
|
|
tmpfs:
|
|
- /tmp
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
cap_drop:
|
|
- ALL
|
|
|
|
secrets:
|
|
secret_key:
|
|
file: ./secrets/secret_key
|
|
oidc_client_secret:
|
|
file: ./secrets/oidc_client_secret
|
|
|
|
volumes:
|
|
mealquest-data:
|
|
name: mealquest-data
|
|
|
|
networks:
|
|
mealquest:
|
|
name: mealquest
|