303 lines
12 KiB
YAML
303 lines
12 KiB
YAML
# ==============================================================================
|
||
# 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"
|