OpenProject is a full project management suite: work packages, Gantt charts, boards, agile backlogs, time tracking, budgets, and multi-project portfolios. It's heavier than the usual self-hosted tools — and the only one of them with real Gantt and time-tracking behaviour that a real organisation will accept.
What it is
Work packages with parent/child relationships, a scheduling engine that pushes dates through a dependency graph, time and cost tracking against budgets, and a notification system. The Community edition covers all of that; the Enterprise layer adds SSO and advanced permissions.
Before you start
- It is a five-container stack: app, worker, proxy, Postgres, and a cache. This is the heaviest app on this page — give it real resources.
- The
seedercontainer is for first-boot admin credentials. It exits after doing its job; that's normal, not a failure. - You need a reverse proxy that sets
X-Forwarded-Proto. Without it every generated link ishttp://and email notifications send broken URLs.
1 — Create the directory
mkdir -p ~/services/openproject/pgdata
cd ~/services/openproject
openssl rand -hex 32
2 — Write the compose file
services:
db:
image: postgres:17-alpine
container_name: openproject-db
restart: unless-stopped
environment:
POSTGRES_USER: openproject
POSTGRES_PASSWORD: CHANGE_ME
POSTGRES_DB: openproject
volumes:
- ./pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U openproject"]
interval: 10s
timeout: 5s
retries: 5
cache:
image: memcached:1.6-alpine
container_name: openproject-cache
restart: unless-stopped
command: memcached -m 256
seeder:
image: openproject/openproject:16
container_name: openproject-seeder
restart: "no"
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://openproject:CHANGE_ME@db/openproject
OPENPROJECT_SEEDER_ADMIN_PASSWORD: CHANGE_ME
OPENPROJECT_HELLO_HOST: projects.example.com
INIT_SEED_URL: https://projects.example.com/
web:
image: openproject/openproject:16
container_name: openproject-web
restart: unless-stopped
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
ports:
- "127.0.0.1:8080:80"
environment:
DATABASE_URL: postgres://openproject:CHANGE_ME@db/openproject
OPENPROJECT_CACHE__URL: memcached://cache:11211
OPENPROJECT_HOST__NAME: projects.example.com
OPENPROJECT_HELLO_HOST: projects.example.com
OPENPROJECT_RAILS__CACHE__STORE: memcache
OPENPROJECT_ATTACHMENTS__MAX__SIZE: 52428800
CRON_ENABLED: "true"
TZ: UTC
worker:
image: openproject/openproject:16
container_name: openproject-worker
restart: unless-stopped
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
environment:
DATABASE_URL: postgres://openproject:CHANGE_ME@db/openproject
OPENPROJECT_CACHE__URL: memcached://cache:11211
OPENPROJECT_RAILS__CACHE__STORE: memcache
WORKER_ENABLED: "true"
CRON_ENABLED: "true"
TZ: UTC
worker separate from webLong jobs — report generation, bulk email, exports, the scheduler's dependency propagation — run in the worker so they never block a web request. With CRON_ENABLED on the web container too, you get scheduled jobs without a third container.3 — Start it
docker compose up -d
docker compose logs -f web | grep -i "booting\|migrat\|error"
docker compose ps seeder # should show Exited (0)
4 — First-run setup
- Open
https://projects.example.comand sign in asadminwith the seeder password. Change it immediately under My account → Settings. - In Administration → Work packages → Types, decide your project's work package types before importing anything. Renaming later means fixing history.
- Turn on Work package description and Activity modules for the projects that need them, and switch to the scoped styles view so each project shows only the fields it uses.
- Add your team under Administration → Users, set global roles, and confirm mail works by assigning yourself a work package and watching for the notification.
- Set the default time zone under Administration → Settings. Gantt scheduling and time tracking both get it wrong for someone else otherwise.
5 — Back it up
docker exec -u postgres openproject-db pg_dump -U openproject openproject | gzip > op-$(date +%F).sql.gz
docker run --rm -v openproject_web_assets:/a -v "$PWD":/b alpine \
tar czf /b/openproject-assets.tgz -C /a .
openproject_web_assets volume, not into Postgres. A database dump alone is not a complete restore — you need that volume too, and they must be from the same point in time.