name: tx-os services: postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER:-tx} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-tx_dev_password} POSTGRES_DB: ${POSTGRES_DB:-tx} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-tx} -d ${POSTGRES_DB:-tx}"] interval: 5s timeout: 5s retries: 20 migrate: build: context: . dockerfile: docker/api-server.Dockerfile image: tx-os/api-server:latest depends_on: postgres: condition: service_healthy environment: NODE_ENV: production DATABASE_URL: postgres://${POSTGRES_USER:-tx}:${POSTGRES_PASSWORD:-tx_dev_password}@postgres:5432/${POSTGRES_DB:-tx} # Seed passwords are now optional. When unset, the seed script # creates roles/permissions only and leaves admin creation to # the first-run Setup Wizard (/api/setup/complete). SEED_ADMIN_PASSWORD: ${SEED_ADMIN_PASSWORD:-} SEED_USER_PASSWORD: ${SEED_USER_PASSWORD:-} SEED_DEMO_MEETINGS: ${SEED_DEMO_MEETINGS:-false} PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:${APP_PORT:-3000}} user: root entrypoint: ["/usr/bin/tini", "--"] command: ["/bin/bash", "/usr/local/bin/migrate.sh"] restart: "no" api: build: context: . dockerfile: docker/api-server.Dockerfile image: tx-os/api-server:latest restart: unless-stopped depends_on: postgres: condition: service_healthy migrate: condition: service_completed_successfully environment: NODE_ENV: production PORT: 8080 DATABASE_URL: postgres://${POSTGRES_USER:-tx}:${POSTGRES_PASSWORD:-tx_dev_password}@postgres:5432/${POSTGRES_DB:-tx} SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required — copy .env.docker.example to .env and edit it} LOCAL_STORAGE_SIGNING_SECRET: ${LOCAL_STORAGE_SIGNING_SECRET:-${SESSION_SECRET}} LOCAL_STORAGE_ROOT: /app/storage STORAGE_DRIVER: ${STORAGE_DRIVER:-local} PRIVATE_OBJECT_DIR: ${PRIVATE_OBJECT_DIR:-/app/storage/private} PUBLIC_OBJECT_SEARCH_PATHS: ${PUBLIC_OBJECT_SEARCH_PATHS:-/app/storage/public} DEFAULT_OBJECT_STORAGE_BUCKET_ID: ${DEFAULT_OBJECT_STORAGE_BUCKET_ID:-tx-local} ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:${APP_PORT:-3000}} PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:${APP_PORT:-3000}} TRUST_PROXY_HTTPS: ${TRUST_PROXY_HTTPS:-false} LOG_LEVEL: ${LOG_LEVEL:-info} HTTPS_MODE: ${HTTPS_MODE:-local} LOCAL_DOMAIN: ${LOCAL_DOMAIN:-} LOCAL_IP: ${LOCAL_IP:-} SMTP_HOST: ${SMTP_HOST:-} SMTP_PORT: ${SMTP_PORT:-} SMTP_USER: ${SMTP_USER:-} SMTP_PASS: ${SMTP_PASS:-} SMTP_FROM: ${SMTP_FROM:-} SMTP_SECURE: ${SMTP_SECURE:-} volumes: - app_storage:/app/storage web: build: context: . dockerfile: docker/tx-os.Dockerfile image: tx-os/web:latest restart: unless-stopped depends_on: - api # No host port binding — Caddy is the single public edge. expose: - "80" caddy: image: caddy:2.8-alpine restart: unless-stopped depends_on: - api - web environment: LOCAL_DOMAIN: ${LOCAL_DOMAIN:-tx.local} LOCAL_IP: ${LOCAL_IP:-127.0.0.1} HTTPS_MODE: ${HTTPS_MODE:-local} # Pick the Caddyfile at boot. We auto-fall-back to the cert-free # Caddyfile.skip when HTTPS_MODE=skip OR when /certs is missing # the keypair, so a fresh `./start.sh` works on hosts that don't # have mkcert without any extra wiring. entrypoint: - /bin/sh - -c - | if [ "$${HTTPS_MODE}" = "skip" ] || [ ! -f /certs/local-cert.pem ] || [ ! -f /certs/local-key.pem ]; then exec caddy run --config /etc/caddy/Caddyfile.skip --adapter caddyfile else exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile fi ports: # HTTP edge: defaults to APP_PORT so the legacy http://localhost:${APP_PORT} # URL keeps working without modifying start.sh. - "${HTTP_PORT:-${APP_PORT:-3000}}:80" # HTTPS edge: HTTPS_PORT is the operator-facing variable used by # both .env.example (default 443, for local-setup.sh users) and # .env.docker.example (default 8443, for start.sh users on hosts # without privileged-port access). When HTTPS_MODE=skip the # entrypoint loads Caddyfile.skip which doesn't listen on :443, # so a high default keeps the bind harmless either way. - "${HTTPS_PORT:-443}:443" volumes: - ./docker/Caddyfile:/etc/caddy/Caddyfile:ro - ./docker/Caddyfile.skip:/etc/caddy/Caddyfile.skip:ro - ./certs:/certs:ro - caddy_data:/data - caddy_config:/config volumes: postgres_data: app_storage: caddy_data: caddy_config: