Skip to Content

Deploy OpenProject with Docker Compose and Traefik on Ubuntu (Production Guide)

A practical, operations-first guide for secure project management hosting with backups, TLS, and day-2 maintenance.

Many teams adopt OpenProject after they outgrow lightweight task boards and need stronger planning features such as Gantt timelines, workload visibility, and portfolio-level governance. The challenge is not just getting OpenProject online; the real challenge is running it reliably for months with clean TLS, predictable backups, and safe upgrade routines that do not disrupt delivery teams. This guide is designed for operators who need production outcomes, not just a demo deployment.

We will deploy OpenProject behind Traefik using Docker Compose on Ubuntu. This pattern works especially well for small and mid-sized teams that want fast iteration with clear operational control. You keep infrastructure simple, but still get secure inbound traffic, centralized reverse proxy behavior, and a repeatable deployment process. Along the way, we will cover practical hardening, secret handling, verification, and troubleshooting grounded in common real-world failures.

The workflow in this tutorial is intentionally opinionated: isolate data paths, enforce least privilege where practical, and automate the tasks that are most likely to fail when done manually under pressure. By the end, you will have a stable baseline you can hand off to another engineer without tribal knowledge.

Architecture and traffic flow overview

The stack uses three containers: Traefik for ingress and certificates, PostgreSQL for persistence, and OpenProject for the application layer. Incoming HTTPS traffic terminates at Traefik. Traefik routes requests to OpenProject over the internal Docker network based on host rules. OpenProject talks only to PostgreSQL and stores uploaded assets on a mounted volume. This creates a clean separation between ingress, state, and application behavior.

Operationally, this design keeps failure domains understandable. Certificate or routing problems are isolated to Traefik. Data integrity and retention are isolated to PostgreSQL and volume backups. Application upgrades stay mostly inside the OpenProject service boundary. With this separation, incident response is faster because logs and symptoms map to a clear layer.

Prerequisites

  • Ubuntu 22.04 or newer with sudo access
  • A DNS A record pointing your guide hostname to the server (example: projects.example.com)
  • Ports 80 and 443 reachable from the internet
  • Basic familiarity with Docker and Linux service operations
  • A backup target strategy (local + remote strongly recommended)

Before touching production, plan your hostname, admin ownership, and expected user load. If you already run other reverse-proxied apps, align Traefik labels and certificate policy with your existing standards to avoid drift.

Step-by-step deployment

1) Prepare the host

Start with package updates and baseline tools. Keeping the host lean reduces update blast radius and makes audits easier.

sudo apt update && sudo apt -y upgrade
sudo apt install -y ca-certificates curl gnupg lsb-release ufw jq

If the copy button does not work in your browser, select the code block and copy manually.

2) Install Docker Engine and Compose plugin

Use Docker's upstream repository for predictable versioning and security updates.

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg

echo   "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker $USER

If the copy button does not work in your browser, select the code block and copy manually.

3) Create project directories and secrets

Use a dedicated working directory under /opt and keep secrets in a root-owned environment file.

mkdir -p /opt/openproject/{data,db,traefik,backups}
cd /opt/openproject

cat > .env <<'EOF'
OPENPROJECT_HOST=projects.example.com
OPENPROJECT_SECRET_KEY_BASE=$(openssl rand -hex 64)
POSTGRES_DB=openproject
POSTGRES_USER=openproject
POSTGRES_PASSWORD=$(openssl rand -base64 32 | tr -d '\n')
EOF

If the copy button does not work in your browser, select the code block and copy manually.

4) Define the full stack in docker-compose.yml

This Compose file wires HTTPS ingress, persistent database storage, and the OpenProject app service with Traefik labels.

cat > docker-compose.yml <<'EOF'
version: "3.9"
services:
  traefik:
    image: traefik:v3.0
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.le.acme.httpchallenge=true
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
      - [email protected]
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik:/letsencrypt
    restart: unless-stopped

  db:
    image: postgres:15
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 10
    restart: unless-stopped

  openproject:
    image: openproject/openproject:14
    depends_on:
      db:
        condition: service_healthy
    environment:
      OPENPROJECT_HTTPS: "true"
      OPENPROJECT_HOST__NAME: ${OPENPROJECT_HOST}
      OPENPROJECT_SECRET_KEY_BASE: ${OPENPROJECT_SECRET_KEY_BASE}
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
    labels:
      - traefik.enable=true
      - traefik.http.routers.openproject.rule=Host(`${OPENPROJECT_HOST}`)
      - traefik.http.routers.openproject.entrypoints=websecure
      - traefik.http.routers.openproject.tls.certresolver=le
      - traefik.http.services.openproject.loadbalancer.server.port=8080
    volumes:
      - ./data:/var/openproject/assets
    restart: unless-stopped
EOF

If the copy button does not work in your browser, select the code block and copy manually.

5) Apply base hardening

Lock down secret file permissions and expose only required firewall ports.

chmod 600 /opt/openproject/.env
sudo chown root:root /opt/openproject/.env
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable

If the copy button does not work in your browser, select the code block and copy manually.

6) Launch and verify container health

Bring up the stack and confirm service states before opening to end users.

cd /opt/openproject
docker compose pull
docker compose up -d
docker compose ps

If the copy button does not work in your browser, select the code block and copy manually.

Configuration and secret-handling best practices

Do not store plaintext credentials in repository files. Keep production secrets in a server-local secret file with strict permissions, or use a dedicated secret manager when available. Rotate database and admin credentials at planned intervals and after any incident involving shell access.

When teams scale, standardize secret naming and rotation metadata (owner, created date, next rotation target). Small discipline here prevents major outages later. Also avoid overloading one account for all integrations: create service-specific identities where possible so revocation does not break unrelated automations.

If you expose metrics or management endpoints, protect them by network policy rather than obscurity. Restrict by source IP and require authentication where supported. Finally, include a change log entry whenever you modify reverse proxy routes or TLS settings; these are frequent root causes during incident reviews.

Verification checklist

Run these checks after first deployment and after each upgrade window. Verification should be routine, not optional.

  • HTTPS endpoint responds with a valid certificate chain
  • OpenProject login page loads without mixed-content warnings
  • Database container is healthy and reachable only internally
  • Backups produce files and restore tests are documented
curl -I https://projects.example.com
openssl s_client -connect projects.example.com:443 -servername projects.example.com < /dev/null | grep -E "subject=|issuer=|Verify return code"

If the copy button does not work in your browser, select the code block and copy manually.

docker compose logs --tail=200 openproject
docker compose exec -T db psql -U openproject -d openproject -c '\dt'

If the copy button does not work in your browser, select the code block and copy manually.

Backups and upgrade operations

Backups are only valuable when restore steps are tested. Store both database dumps and uploaded assets, then validate restoration into a disposable environment at least monthly. Keep retention policies explicit and tied to business requirements.

cat > /opt/openproject/backup.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd /opt/openproject
source .env
TS=$(date +%F-%H%M)
mkdir -p backups/$TS

docker compose exec -T db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" | gzip > backups/$TS/db.sql.gz
tar -czf backups/$TS/assets.tar.gz data/
find backups -mindepth 1 -maxdepth 1 -type d | sort | head -n -7 | xargs -r rm -rf
EOF

chmod +x /opt/openproject/backup.sh
( crontab -l 2>/dev/null; echo "30 2 * * * /opt/openproject/backup.sh" ) | crontab -

If the copy button does not work in your browser, select the code block and copy manually.

For upgrades, pull new images, run application migrations/setup, and then restart services in controlled order. Schedule upgrades during low-traffic windows and keep rollback notes ready.

# zero-downtime-ish upgrade routine
cd /opt/openproject
docker compose pull
docker compose run --rm openproject ./docker/prod/setup

docker compose up -d

If the copy button does not work in your browser, select the code block and copy manually.

After upgrading, verify application login, project listing, attachments, and at least one timeline/Gantt view. Functional smoke tests catch regressions that simple health checks miss.

Common issues and practical fixes

Traefik issues certificate but browser still warns

Most often this is DNS propagation lag, stale CDN edge cache, or a hostname mismatch. Re-check A/AAAA records and verify that the requested host exactly matches Traefik router rules. Confirm no old reverse proxy is still bound to 443.

OpenProject starts but responds slowly

Inspect host memory pressure and database I/O latency first. In many deployments, slow response traces back to undersized VM storage or noisy neighbors rather than application bugs. Track container CPU throttling and adjust limits conservatively.

Random 502 errors after updates

Usually a startup ordering issue or failed migration left the app unhealthy while Traefik kept routing traffic. Check compose logs, rerun setup/migration, and only then re-enable traffic. Add health-check aware deploy sequencing to reduce recurrence.

Backups exist but restores fail

Common causes include version mismatch between PostgreSQL client/server tools or missing asset archives. Version-pin restore tooling and keep a documented drill script. Treat restore tests as part of release readiness.

FAQ

Can I run OpenProject and Traefik on the same host in production?

Yes, for many SMB workloads this is a pragmatic starting point. Just enforce backup discipline, monitor host utilization, and define a migration path to split roles if load grows.

Should I use Docker secrets instead of an .env file?

If your platform and team workflow support it, Docker secrets are preferable for sensitive values. For single-host setups, a root-owned .env file with strict permissions is acceptable when paired with rotation and audit discipline.

How often should I back up OpenProject?

At minimum daily for low-change environments; for active teams, use multiple backups per day. Retention should reflect your RPO/RTO targets and compliance requirements.

What is the safest upgrade approach?

Use a staged process: backup, pull images, run setup/migrations, restart, then run a verification checklist. Keep rollback artifacts for one previous stable version.

Can I front this stack with Cloudflare?

Yes, but avoid conflicting TLS termination settings. Decide where TLS terminates, keep certificate behavior consistent, and test websocket/proxy headers if enabled.

How do I prevent duplicate notifications or mail issues?

Set a canonical external URL, verify outbound mail settings, and ensure only one instance handles background jobs unless your architecture explicitly supports multi-worker patterns.

When should I move from Compose to Kubernetes?

Move when you need strong multi-node scheduling, advanced policy controls, or organizational standardization. For many teams, Compose remains simpler and faster to operate until scale genuinely demands orchestration complexity.

Related guides

For deeper platform operations, review these related guides from our library:

Talk to us

Need help deploying MinIO in production, designing resilient storage architecture, or building secure backup and upgrade runbooks your team can trust? We can help with architecture, hardening, migration, and operational readiness.

Contact Us

Clipboard helper (optional)

If your Odoo theme strips scripts, the Copy button may not execute JavaScript. Manual copy fallback text is already provided under each code block.

Production Guide: Deploy MinIO with Kubernetes + Helm + cert-manager + NGINX Ingress on Ubuntu
A production-focused deployment playbook with secure secrets handling, day-2 operations, verification, and incident-ready troubleshooting.