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.compointing 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 <<'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 <<'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 <<'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
- Deploy Forgejo with Docker Compose + Caddy + PostgreSQL + SSH
- Deploy Outline with Docker Compose + Caddy + PostgreSQL + Redis
- Deploy Healthchecks with Docker Compose + Caddy + PostgreSQL
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.