Kituwa IT · Self-hosting guides

Install OpenProject with Docker Compose

Self-hosted project management with Gantt

project managementGanttagile

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.

Category
Productivity
License
GPL-3.0
Needs
Postgres + memcached
Image
openproject/openproject

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 seeder container 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 is http:// and email notifications send broken URLs.

1 — Create the directory

Command / configuration
mkdir -p ~/services/openproject/pgdata
cd ~/services/openproject
openssl rand -hex 32

2 — Write the compose file

Command / configuration
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
Why there's a 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

Command / configuration
docker compose up -d
docker compose logs -f web | grep -i "booting\|migrat\|error"
docker compose ps seeder   # should show Exited (0)

First boot runs database migrations and takes several minutes. Wait for "Application booted" before touching the URL.

4 — First-run setup

  1. Open https://projects.example.com and sign in as admin with the seeder password. Change it immediately under My account → Settings.
  2. In Administration → Work packages → Types, decide your project's work package types before importing anything. Renaming later means fixing history.
  3. 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.
  4. Add your team under Administration → Users, set global roles, and confirm mail works by assigning yourself a work package and watching for the notification.
  5. 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

Command / configuration
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 .
Attachments live outside the databaseUploaded files go into the 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.

Help when you need it

Want help getting this running?

We can help with a supported Linux host, application setup, migration or troubleshooting. Contact us to confirm the software, scope and scheduling before ordering.

Related guides

← Browse all 30 guides · Back to top