Skip to Content

Production Guide: Deploy OpenProject with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu

A production-oriented OpenProject deployment with automatic HTTPS, durable storage, backups, verification, and troubleshooting.

OpenProject is a practical fit when a team wants project planning, issue tracking, roadmaps, and documentation in one self-hosted workspace without sending delivery data to a SaaS vendor. This guide walks through a production-oriented OpenProject deployment on Ubuntu using Docker Compose, Caddy, PostgreSQL, and Redis. The pattern is intentionally simple: one VM, clear persistent volumes, an automatic HTTPS reverse proxy, database backups, and repeatable verification commands.

The scenario is a small engineering or operations team that needs a private planning portal at projects.example.com. The same layout also works for client delivery teams, internal IT queues, implementation roadmaps, and compliance projects where the work packages, files, and comments should remain under your own control.

Architecture and flow overview

The deployment has four moving parts. Caddy terminates TLS and forwards browser traffic to the OpenProject web container. OpenProject stores relational data in PostgreSQL, keeps background job state and caches in Redis, and writes attachments plus generated assets to a persistent application volume. Docker Compose keeps the service definitions versioned so upgrades are predictable.

  • Browser to Caddy: HTTPS on ports 80 and 443, with automatic certificate renewal.
  • Caddy to OpenProject: private Docker network traffic on container port 8080.
  • OpenProject to PostgreSQL: durable database state in a named volume.
  • OpenProject to Redis: queue and cache support for responsive background processing.
  • Backups: scheduled database dumps plus application file volume snapshots.

This single-node model is easy to operate, but it still separates public ingress from stateful services. If you later need higher availability, the same boundaries help you move PostgreSQL to a managed database, place files on object storage, or run the application behind a larger ingress layer.

Prerequisites

  • Ubuntu 22.04 or 24.04 server with at least 2 vCPU, 4 GB RAM, and 40 GB disk.
  • A DNS record such as projects.example.com pointing to the server.
  • Ports 80 and 443 open from the internet.
  • Docker Engine and the Docker Compose plugin installed.
  • SMTP credentials for notifications and password reset emails.
  • A backup target such as S3-compatible storage, rsync storage, or another secure server.

Step-by-step deployment

1. Create the application directory

Keep the Compose file, environment file, Caddy configuration, and operational scripts together. That makes audits and handoffs much easier.

sudo mkdir -p /opt/openproject/{caddy,backups}
sudo chown -R $USER:$USER /opt/openproject
cd /opt/openproject
umask 077
touch .env

If the copy button is unavailable, manually select the command block and copy it.

2. Define secrets and application settings

Generate unique values for database credentials, the OpenProject secret key base, and SMTP passwords. Do not reuse credentials from another service.

cat > .env <<'EOF'
OPENPROJECT_HOST=projects.example.com
OPENPROJECT_HTTPS=true
OPENPROJECT_SECRET_KEY_BASE=replace_with_64_plus_random_characters
POSTGRES_DB=openproject
POSTGRES_USER=openproject
POSTGRES_PASSWORD=replace_with_database_password
REDIS_PASSWORD=replace_with_redis_password
SMTP_HOST=smtp.example.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=replace_with_smtp_password
SMTP_DOMAIN=example.com
SMTP_AUTH=login
SMTP_ENABLE_STARTTLS_AUTO=true
EOF
openssl rand -hex 48
openssl rand -base64 32
openssl rand -base64 32

If the copy button is unavailable, manually select the command block and copy it.

3. Add the Docker Compose stack

The Compose file below pins the service boundaries, health checks, volumes, and restart policy. It does not publish PostgreSQL or Redis to the host, which reduces accidental exposure.

cat > docker-compose.yml <&lt'EOF'
services:
  caddy:
    image: caddy:2.8
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      openproject:
        condition: service_started
    networks: [public, private]

  openproject:
    image: openproject/openproject:15
    restart: unless-stopped
    env_file: .env
    environment:
      OPENPROJECT_HOST__NAME: ${OPENPROJECT_HOST}
      OPENPROJECT_HTTPS: ${OPENPROJECT_HTTPS}
      OPENPROJECT_SECRET_KEY_BASE: ${OPENPROJECT_SECRET_KEY_BASE}
      OPENPROJECT_DATABASE__URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      OPENPROJECT_CACHE__REDIS__URL: redis://:${REDIS_PASSWORD}@redis:6379/0
      OPENPROJECT_EMAIL__DELIVERY__METHOD: smtp
      OPENPROJECT_SMTP__ADDRESS: ${SMTP_HOST}
      OPENPROJECT_SMTP__PORT: ${SMTP_PORT}
      OPENPROJECT_SMTP__USER__NAME: ${SMTP_USER}
      OPENPROJECT_SMTP__PASSWORD: ${SMTP_PASSWORD}
      OPENPROJECT_SMTP__DOMAIN: ${SMTP_DOMAIN}
      OPENPROJECT_SMTP__AUTHENTICATION: ${SMTP_AUTH}
      OPENPROJECT_SMTP__ENABLE__STARTTLS__AUTO: ${SMTP_ENABLE_STARTTLS_AUTO}
    volumes:
      - openproject_assets:/var/openproject/assets
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks: [private]

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

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks: [private]

volumes:
  postgres_data:
  openproject_assets:
  caddy_data:
  caddy_config:

networks:
  public:
  private:
    internal: true
EOF

If the copy button is unavailable, manually select the command block and copy it.

4. Configure Caddy

Caddy should be the only public entry point. The proxy headers preserve the original protocol and client information for OpenProject.

cat > caddy/Caddyfile <&lt'EOF'
{$OPENPROJECT_HOST} {
  encode zstd gzip
  reverse_proxy openproject:8080 {
    header_up X-Forwarded-Proto https
    header_up X-Forwarded-Host {host}
    header_up X-Real-IP {remote_host}
  }
  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    X-Content-Type-Options "nosniff"
    Referrer-Policy "strict-origin-when-cross-origin"
  }
}
EOF

If the copy button is unavailable, manually select the command block and copy it.

5. Start and initialize the stack

Pull the images, start the services, and watch the logs until migrations finish. The first boot can take several minutes while OpenProject prepares the database.

docker compose pull
docker compose up -d
docker compose ps
docker compose logs -f --tail=120 openproject

If the copy button is unavailable, manually select the command block and copy it.

Configuration and secrets handling best practices

Treat .env as a secret file. Keep it readable only by the deployment operator and root, exclude it from Git, and store a sealed copy in your password manager. If a secret is exposed, rotate the affected value and restart the stack. For SMTP, use a dedicated mailbox or API credential rather than a personal account.

OpenProject has many useful administrative settings after the first login. Set the public host name, verify email delivery, configure user invitation rules, review attachment size limits, and create at least one additional administrator account. Enable two-factor authentication for administrators if your identity policy supports it. For client-facing portals, define a clear project template so new workspaces start with the same work package types, roles, and notification defaults.

For backups, capture both PostgreSQL and the OpenProject asset volume. Database-only backups are not enough because file attachments, avatars, and generated assets live outside the database. Test restores regularly on a separate host; a backup process that has never been restored is only a hope.

cat > backups/backup-openproject.sh <&lt'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd /opt/openproject
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p backups/out
source .env
docker compose exec -T postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" | gzip > "backups/out/openproject-db-$stamp.sql.gz"
docker run --rm -v openproject_openproject_assets:/assets:ro -v "$PWD/backups/out:/backup" alpine tar -czf "/backup/openproject-assets-$stamp.tar.gz" -C /assets .
find backups/out -type f -mtime +14 -delete
EOF
chmod +x backups/backup-openproject.sh
./backups/backup-openproject.sh

If the copy button is unavailable, manually select the command block and copy it.

Verification checklist

Run verification from both the server and an outside workstation. You want to prove that the site is reachable, TLS works, containers are healthy, and the application can send mail.

docker compose ps
curl -I https://projects.example.com
docker compose exec -T postgres pg_isready -U openproject -d openproject
docker compose exec -T redis redis-cli -a "$REDIS_PASSWORD" ping
docker compose logs --tail=80 caddy
docker compose logs --tail=120 openproject

If the copy button is unavailable, manually select the command block and copy it.

  • The HTTP response should be 200, 302, or another expected OpenProject response, not a proxy error.
  • Caddy logs should show certificate issuance or renewal without repeated challenge failures.
  • PostgreSQL and Redis health checks should be healthy in docker compose ps.
  • Password reset or invitation email should arrive through the configured SMTP service.
  • A test project should allow work package creation, attachment upload, and comment notifications.

Common issues and fixes

Caddy cannot issue a certificate

Confirm DNS points to the server, ports 80 and 443 are reachable, and another service is not already bound to those ports. If your firewall only allows 443, temporarily open 80 for the ACME HTTP challenge or configure an alternate DNS challenge workflow.

OpenProject redirects to HTTP or shows mixed content

Check OPENPROJECT_HOST__NAME, OPENPROJECT_HTTPS, and the proxy headers in the Caddyfile. Restart the application after changing environment variables.

Login works but background jobs feel slow

Inspect Redis connectivity and container memory pressure. Small instances can run OpenProject, but heavy imports, many attachments, or multiple active projects may require more RAM.

Email invitations fail

Recheck SMTP host, port, authentication mode, and STARTTLS setting. Many providers require an app password or API credential. Also verify SPF, DKIM, and DMARC for the sender domain to avoid silent spam filtering.

Backups are too large

Review attachment retention, old exports, and project archive policy. Store backups outside the server and apply lifecycle rules on the remote target rather than only deleting local copies.

FAQ

Can I run OpenProject without Redis?

For a production-style setup, keep Redis. It improves background job and cache behavior and gives you a clearer operational boundary than relying on defaults inside the application container.

Should PostgreSQL be on the same server?

For small teams, local PostgreSQL is acceptable and easy to back up. Move it to a managed or dedicated database when you need point-in-time recovery, stronger isolation, or higher availability.

How often should I back up the stack?

Daily backups are a reasonable baseline. Increase frequency if OpenProject becomes the source of truth for active delivery, compliance evidence, or client approvals.

How do I upgrade safely?

Read the OpenProject release notes, take a fresh database and asset backup, pull the new image tag in a staging environment, verify migrations, then repeat the same process in production during a maintenance window.

Can I use an existing external Caddy or NGINX proxy?

Yes. Remove the bundled Caddy service, connect OpenProject to the proxy network, and preserve the forwarded host and protocol headers. Keep PostgreSQL and Redis private.

What monitoring should I add first?

Start with uptime checks for the public URL, disk usage alerts, container restart alerts, backup job success alerts, and log review for repeated 500 errors or SMTP failures.

Is this design suitable for regulated teams?

It can be a foundation, but regulated teams should add centralized logging, formal backup retention, vulnerability scanning, documented access reviews, and a tested restore runbook.

Internal links

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

Production Guide: Deploy Passbolt with Docker Compose + Caddy + MariaDB + GnuPG on Ubuntu
A production-oriented Passbolt deployment for teams that need controlled credential sharing, HTTPS, backups, and recovery checks.