# Packet Station base stack: postgres, a one-shot migrate, the app and the worker. # One file for test and production. The env file picks the stack. # # Production on docker-1: # docker compose -p packetstation --env-file /etc/packetstation/prod/stack.env \ # -f deploy/compose.yaml -f deploy/compose.proxy.yaml up -d # Test on docker-1: # docker compose -p packetstation-test --env-file /etc/packetstation/test/stack.env \ # -f deploy/compose.yaml -f deploy/compose.proxy.yaml up -d # # No service publishes a port. NPM reaches the app over npm_default (compose.proxy.yaml). # PostgreSQL is only on the internal backend network. # # ClamAV is optional. Set CLAMAV_HOST=packetstation-clamav in the env file and add `--profile clamav` # to every `docker compose` command of that stack. Without the profile the clamav service does not run. # The project name comes from the env file so prod and test can never share volumes by accident. # `docker compose -p` still wins when given. name: ${PROJECT_NAME:?set PROJECT_NAME in the stack env file} x-logging: &logging driver: json-file options: max-size: "10m" max-file: "5" # Hardening shared by the three image services. The root filesystem is read-only; # /tmp is a small tmpfs for Python temp files and the worker beat file. x-hardening: &hardening read_only: true tmpfs: - /tmp:size=64m,mode=1777 security_opt: - no-new-privileges:true cap_drop: - ALL services: postgres: # postgres:16-bookworm pinned by digest (multi-arch index, looked up 2026-10-15). # To refresh: docker buildx imagetools inspect postgres:16-bookworm # and copy the top-level "Digest:" line here. Test on the test stack first. image: postgres:16-bookworm@sha256:0ea6700a3b4f0ae6ce746519073558aed4d88a79d8d07622a9a644946c7319c4 # uid 999 is the image's postgres user. Group 10001 lets it read the # root:10001 0440 secret files without running as root. user: "999:10001" environment: POSTGRES_DB: postgres # 01-roles.sh creates the packetstation database. POSTGRES_USER: postgres POSTGRES_PASSWORD_FILE: /run/secrets/postgres_superuser_password PGDATA: /var/lib/postgresql/data/pgdata TZ: UTC secrets: - postgres_superuser_password - postgres_migrate_password - postgres_app_password volumes: - pgdata:/var/lib/postgresql/data - ./postgres/initdb:/docker-entrypoint-initdb.d:ro networks: backend: aliases: - packetstation-postgres # PG_HOST. Unique so it cannot collide with another stack. shm_size: 256m security_opt: - no-new-privileges:true healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d postgres -h 127.0.0.1"] interval: 10s timeout: 5s retries: 6 start_period: 30s restart: unless-stopped logging: *logging migrate: image: ${IMAGE:?set IMAGE in the stack env file} command: ["migrate"] env_file: - ${ENV_FILE:?set ENV_FILE in the stack env file} environment: # Fixed in-container paths. These win over the env file, where BACKUP_DIR is the host path. ASSETS_DIR: /data/assets BACKUP_DIR: /backups STATIC_DIR: /app/static secrets: - postgres_migrate_password - postgres_app_password networks: backend: {} depends_on: postgres: condition: service_healthy <<: *hardening restart: "no" logging: *logging app: image: ${IMAGE:?set IMAGE in the stack env file} command: ["app"] env_file: - ${ENV_FILE:?set ENV_FILE in the stack env file} environment: # Fixed in-container paths. These win over the env file, where BACKUP_DIR is the host path. ASSETS_DIR: /data/assets BACKUP_DIR: /backups STATIC_DIR: /app/static secrets: - postgres_app_password - ldap_bind_password - session_key - seal_key volumes: - assets:/data/assets - ${CA_BUNDLE_FILE:?set CA_BUNDLE_FILE in the stack env file}:/etc/ssl/pesco/ca.pem:ro networks: backend: {} egress: {} depends_on: migrate: condition: service_completed_successfully healthcheck: test: ["CMD", "packetstation", "healthcheck", "app"] interval: 30s timeout: 5s retries: 3 start_period: 30s <<: *hardening mem_limit: 1g restart: unless-stopped logging: *logging worker: image: ${IMAGE:?set IMAGE in the stack env file} command: ["worker"] env_file: - ${ENV_FILE:?set ENV_FILE in the stack env file} environment: # Fixed in-container paths. These win over the env file, where BACKUP_DIR is the host path. ASSETS_DIR: /data/assets BACKUP_DIR: /backups STATIC_DIR: /app/static secrets: - postgres_app_password - visualkit_password - mfileskit_password - ldap_bind_password - seal_key volumes: - assets:/data/assets - ${CA_BUNDLE_FILE:?set CA_BUNDLE_FILE in the stack env file}:/etc/ssl/pesco/ca.pem:ro - ${BACKUP_DIR:?set BACKUP_DIR in the stack env file}:/backups:ro networks: backend: {} egress: {} depends_on: migrate: condition: service_completed_successfully healthcheck: test: ["CMD", "packetstation", "healthcheck", "worker"] interval: 30s timeout: 5s retries: 3 start_period: 60s <<: *hardening mem_limit: 1g restart: unless-stopped logging: *logging clamav: # Optional virus scanner for uploads (profile clamav). clamd listens on 3310 on the backend network. # Pinned to the 1.4 minor tag, not a digest: the tag follows the 1.4 patch releases. # To pin a digest: docker buildx imagetools inspect clamav/clamav:1.4 and use the top-level "Digest:". image: clamav/clamav:1.4 profiles: ["clamav"] environment: CLAMAV_NO_MILTERD: "true" volumes: - clamav-db:/var/lib/clamav # Signatures. The first start downloads them (a few minutes). networks: backend: aliases: - packetstation-clamav # CLAMAV_HOST. Unique so it cannot collide with another stack. egress: {} # freshclam needs the internet for signature updates. security_opt: - no-new-privileges:true healthcheck: test: ["CMD", "clamdcheck.sh"] interval: 60s timeout: 10s retries: 3 start_period: 10m # Room for the first signature download and load. mem_limit: 2g restart: unless-stopped logging: *logging networks: backend: internal: true # No route out. postgres lives only here. egress: driver: bridge # Outbound to VISUAL, M-Files, LDAP and SMTP. volumes: pgdata: {} assets: {} clamav-db: {} # Host files, root:10001 mode 0440, one value per file, no trailing carriage return. secrets: postgres_superuser_password: file: ${SECRETS_DIR:?set SECRETS_DIR in the stack env file}/postgres_superuser_password postgres_migrate_password: file: ${SECRETS_DIR}/postgres_migrate_password postgres_app_password: file: ${SECRETS_DIR}/postgres_app_password visualkit_password: file: ${SECRETS_DIR}/visualkit_password mfileskit_password: file: ${SECRETS_DIR}/mfileskit_password ldap_bind_password: file: ${SECRETS_DIR}/ldap_bind_password session_key: file: ${SECRETS_DIR}/session_key seal_key: file: ${SECRETS_DIR}/seal_key