# ============================================================================== # OpenProject — Docker Compose (stable/17) # With SMTP outbound-email support # ============================================================================== # Based on: # https://www.openproject.org/docs/installation-and-operations/installation/docker-compose/ # https://github.com/opf/openproject-docker-compose (branch: stable/17) # https://www.openproject.org/docs/installation-and-operations/configuration/outbound-emails/ # https://www.openproject.org/docs/installation-and-operations/configuration/environment/ # # Usage: # 1. cp .env.example .env # then edit .env with your real values # 2. sudo mkdir -p /var/openproject/assets # 3. sudo chown 1000:1000 -R /var/openproject/assets # 4. docker compose up -d --build --pull always # # SMTP variables live in the .env file (see .env.example). # They are injected into every OpenProject container via the x-op-app anchor. # ============================================================================== version: "3.7" # --------------------------------------------------------------------------- # Networks – frontend faces the proxy; backend is DB / cache only. # --------------------------------------------------------------------------- networks: frontend: backend: # --------------------------------------------------------------------------- # Volumes – persisted between restarts / upgrades. # pgdata – PostgreSQL WAL + tables # opdata – uploaded attachments & assets # --------------------------------------------------------------------------- volumes: pgdata: opdata: # =========================================================================== # YAML Anchors – shared restart policy, image tag, and environment block. # =========================================================================== x-op-restart-policy: &restart_policy restart: unless-stopped x-op-image: &image image: openproject/openproject:${TAG:-17-slim} # --------------------------------------------------------------------------- # x-op-app – merged into every OpenProject container. # All SMTP_* variables are pulled from the .env file so that secrets are # never hard-coded in this file. See .env.example for every placeholder. # --------------------------------------------------------------------------- x-op-app: &app <<: [*image, *restart_policy] environment: # --- Core OpenProject ------------------------------------------------ OPENPROJECT_HTTPS: "${OPENPROJECT_HTTPS:-true}" OPENPROJECT_HOST__NAME: "${OPENPROJECT_HOST__NAME:-localhost:8080}" OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}" OPENPROJECT_EDITION: "${OPENPROJECT_EDITION:-standard}" # Allow hocuspocus to reach the web container by its service name OPENPROJECT_ADDITIONAL__HOST__NAMES: "${OPENPROJECT_ADDITIONAL__HOST__NAMES:-web}" # --- Database --------------------------------------------------------- DATABASE_URL: "${DATABASE_URL:-postgres://postgres:${POSTGRES_PASSWORD:-p4ssw0rd}@db/openproject?pool=20&encoding=unicode&reconnect=true}" # --- Cache ------------------------------------------------------------ OPENPROJECT_CACHE__MEMCACHE__SERVER: "cache:11211" OPENPROJECT_RAILS__CACHE__STORE: "memcache" # --- Threads ---------------------------------------------------------- RAILS_MIN_THREADS: "${RAILS_MIN_THREADS:-4}" RAILS_MAX_THREADS: "${RAILS_MAX_THREADS:-16}" # --- Collaborative editing (Hocuspocus) ------------------------------ OPENPROJECT_COLLABORATIVE__EDITING__HOCUSPOCUS__URL: "${COLLABORATIVE_SERVER_URL:-wss://${OPENPROJECT_HOST__NAME}/hocuspocus}" OPENPROJECT_COLLABORATIVE__EDITING__HOCUSPOCUS__SECRET: "${COLLABORATIVE_SERVER_SECRET:-OVERRIDE_ME_PLEASE}" # --- Inbound email (IMAP) – disabled by default ----------------------- IMAP_ENABLED: "${IMAP_ENABLED:-false}" # ================================================================ # SMTP – Outbound e-mail configuration # ================================================================ EMAIL_DELIVERY_METHOD: "${EMAIL_DELIVERY_METHOD:-smtp}" SMTP_ADDRESS: "${SMTP_ADDRESS}" SMTP_PORT: "${SMTP_PORT:-587}" SMTP_DOMAIN: "${SMTP_DOMAIN}" SMTP_AUTHENTICATION: "${SMTP_AUTHENTICATION:-plain}" SMTP_USER_NAME: "${SMTP_USER_NAME}" SMTP_PASSWORD: "${SMTP_PASSWORD}" SMTP_ENABLE_STARTTLS_AUTO: "${SMTP_ENABLE_STARTTLS_AUTO:-true}" SMTP_SSL: "${SMTP_SSL:-false}" SMTP_TIMEOUT: "${SMTP_TIMEOUT:-5}" OPENPROJECT_MAILER__FROM__ADDRESS: "${MAILER_FROM_ADDRESS:-openproject@example.com}" volumes: - "${OPDATA:-opdata}:/var/openproject/assets" # =========================================================================== # Services # =========================================================================== services: # ----------------------------------------------------------------------- # db – PostgreSQL 16 # ----------------------------------------------------------------------- db: <<: *restart_policy image: postgres:16-alpine networks: - backend volumes: - pgdata:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-p4ssw0rd}" POSTGRES_DB: openproject POSTGRES_USER: postgres healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d openproject"] interval: 10s timeout: 5s retries: 5 # ----------------------------------------------------------------------- # cache – Memcached # ----------------------------------------------------------------------- cache: <<: *restart_policy image: memcached:alpine networks: - backend # ----------------------------------------------------------------------- # seeder – one-shot container # # FIX: Must NOT inherit the restart policy from x-op-app. The seeder is a # one-shot job that exits with code 0 on success. If restart: unless-stopped # is in effect Docker will keep restarting it and it will appear permanently # "waiting" to dependent services that expect service_completed_successfully. # # We use the *image anchor only (no *restart_policy) and set # restart: "no" explicitly. # ----------------------------------------------------------------------- seeder: <<: *image restart: "no" environment: # Minimal env required for seeder (DB + cache) OPENPROJECT_HTTPS: "${OPENPROJECT_HTTPS:-true}" OPENPROJECT_HOST__NAME: "${OPENPROJECT_HOST__NAME:-localhost:8080}" OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}" OPENPROJECT_EDITION: "${OPENPROJECT_EDITION:-standard}" DATABASE_URL: "${DATABASE_URL:-postgres://postgres:${POSTGRES_PASSWORD:-p4ssw0rd}@db/openproject?pool=20&encoding=unicode&reconnect=true}" OPENPROJECT_CACHE__MEMCACHE__SERVER: "cache:11211" OPENPROJECT_RAILS__CACHE__STORE: "memcache" RAILS_MIN_THREADS: "${RAILS_MIN_THREADS:-4}" RAILS_MAX_THREADS: "${RAILS_MAX_THREADS:-16}" volumes: - "${OPDATA:-opdata}:/var/openproject/assets" networks: - backend command: ["./docker/prod/seeder"] depends_on: db: condition: service_healthy cache: condition: service_started # ----------------------------------------------------------------------- # web – Puma application server # # FIX: healthcheck timings tightened to match official stable/17 repo # (interval 10s / timeout 3s / retries 3 / start_period 30s). # The previous values (30s/5s/5/60s) caused downstream containers that # depend on service_healthy to wait far too long, making them appear stuck. # ----------------------------------------------------------------------- web: <<: *app networks: - frontend - backend command: ["./docker/prod/web"] hostname: "${OPENPROJECT_HOST__NAME:-localhost:8080}" depends_on: db: condition: service_healthy cache: condition: service_started seeder: condition: service_completed_successfully labels: - autoheal=true healthcheck: test: - "CMD" - "curl" - "-f" - "http://localhost:8080${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}/health_checks/default" interval: 10s timeout: 3s retries: 3 start_period: 30s expose: - "8080" # ----------------------------------------------------------------------- # autoheal – automatically restarts unhealthy containers # FIX: Added from official stable/17 compose. Without autoheal, an # unhealthy web container (e.g. stuck in a loop) is never restarted even # though Docker marks it unhealthy, which can block the seeder indirectly. # ----------------------------------------------------------------------- autoheal: image: willfarrell/autoheal:1.2.0 volumes: - "/var/run/docker.sock:/var/run/docker.sock" environment: AUTOHEAL_CONTAINER_LABEL: autoheal AUTOHEAL_START_PERIOD: 600 AUTOHEAL_INTERVAL: 30 # ----------------------------------------------------------------------- # worker – Active Job background processor # ----------------------------------------------------------------------- worker: <<: *app networks: - frontend # needs outbound access for SMTP - backend command: ["./docker/prod/worker"] depends_on: db: condition: service_healthy cache: condition: service_started seeder: condition: service_completed_successfully # Explicit DNS – prevents "Network is unreachable" when the container # tries to connect to an external SMTP server. See OP#44515. dns: - "8.8.8.8" # ----------------------------------------------------------------------- # cron – periodic background tasks (e.g. sending digest emails) # FIX: Added from official stable/17 compose. Missing cron can prevent # certain background jobs from running, which can mask seeder-related # issues or cause incomplete initialisation. # ----------------------------------------------------------------------- cron: <<: *app networks: - backend command: ["./docker/prod/cron"] depends_on: db: condition: service_healthy cache: condition: service_started seeder: condition: service_completed_successfully # ----------------------------------------------------------------------- # proxy – Caddy reverse proxy # ----------------------------------------------------------------------- proxy: <<: *restart_policy image: openproject/openproject:${TAG:-17-slim} networks: - frontend ports: - "${PORT:-8080}:80" volumes: - "${OPDATA:-opdata}:/var/openproject/assets" command: ["./docker/prod/proxy"] depends_on: web: condition: service_healthy environment: OPENPROJECT_HTTPS: "${OPENPROJECT_HTTPS:-true}" OPENPROJECT_HOST__NAME: "${OPENPROJECT_HOST__NAME:-localhost:8080}" OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}" # ----------------------------------------------------------------------- # hocuspocus – WebSocket collaboration server # ----------------------------------------------------------------------- hocuspocus: <<: *restart_policy image: openproject/openproject:${TAG:-17-slim} networks: - frontend - backend command: ["./docker/prod/hocuspocus"] depends_on: web: condition: service_healthy environment: OPENPROJECT_COLLABORATIVE__EDITING__HOCUSPOCUS__SECRET: "${COLLABORATIVE_SERVER_SECRET:-OVERRIDE_ME_PLEASE}" expose: - "3000"