Practical guide with Docker or Podman Compose, PostgreSQL, Memcached, Hocuspocus, Nginx reverse proxy and TLS

1. Introduction

OpenProject is a powerful open-source platform for project management, work packages, roadmaps, agile boards, Gantt charts, team collaboration and documentation. It is especially useful for organizations that want to manage projects transparently while keeping control over their own infrastructure.

In the context of digital sovereignty, OpenProject has become an important building block. Many organizations are looking for alternatives to proprietary project management suites and classic Atlassian environments. Together with XWiki, OpenProject can form a strong open-source collaboration stack: OpenProject covers project planning, tasks, roadmaps and work packages, while XWiki provides structured knowledge management and technical documentation.

This article describes a manual OpenProject installation as a container-based deployment. The focus is deliberately not on the older package-based installation. OpenProject now recommends Container-based installation, and Kubernetes deployments are supported through Helm for scalable environments. Package-based installations are still available for selected distributions, but they are no longer the best long-term default for new Linux platforms.

For this guide, we use the following example values:

  • Domain: projects.example.org
  • Runtime user: openproject
  • Public URL: https://projects.example.org
  • OpenProject image: openproject/openproject:17-slim
  • PostgreSQL: version 17
  • internal OpenProject port: 127.0.0.1:6000
  • internal Hocuspocus port: 127.0.0.1:6001
  • first administrator: Anna Berger
  • project manager: Max Schneider

Automation with Ansible will be covered in a follow-up article. This article focuses on the manual deployment so that every component remains understandable.

2. Goal of This Article

The goal is to build a production-oriented OpenProject installation with a clean separation of responsibilities. OpenProject should not run as a quick all-in-one test container. Instead, it should run as a Compose stack with separate services.

The setup consists of:

  • OpenProject Web
  • OpenProject Worker
  • OpenProject Cron
  • OpenProject Seeder
  • PostgreSQL
  • Memcached
  • optional Hocuspocus for collaborative editing
  • Nginx as the reverse proxy
  • TLS for public access

This structure makes the deployment easier to understand, maintain and troubleshoot. Web requests, background jobs, scheduled tasks, database, cache and real-time collaboration components can be observed and debugged separately.

3. Why Containers Instead of Classic Packages?

OpenProject still provides package-based installations for selected distributions. At the same time, the official documentation clearly recommends Container-based installation. OpenProject also states that some future features are planned to be delivered only for Container-based installations and that no new package repositories are planned for newer Linux versions.

For new production environments, a container-based deployment is therefore the more future-proof option. It reduces dependencies on the host operating system, simplifies updates and fits better into modern operating models based on Docker Compose, Podman Compose or Kubernetes.

The classic package installation can still be useful when:

  • an officially supported distribution is used
  • the server is dedicated to OpenProject
  • the OpenProject package installer should manage the local configuration
  • existing operational processes prefer DEB or RPM packages

For new environments, however, a container-based setup is the more flexible choice. This guide therefore uses a manual Compose deployment.

4. Target Architecture

The target architecture separates public access, application services and persistent data.

bash
Client / Browser
|
| HTTPS
v
Nginx Reverse Proxy
|
| HTTP lokal :6000
v
OpenProject Web
|
+--> OpenProject Worker
+--> OpenProject Cron
+--> Memcached
+--> PostgreSQL
|
| WebSocket /hocuspocus
v
Hocuspocus

Nginx is the only public-facing component. The OpenProject web container binds only to 127.0.0.1:6000. Hocuspocus, if enabled, binds only to 127.0.0.1:6001. PostgreSQL and Memcached remain inside the internal Compose network.

Important principles:

  • no public exposure of container ports
  • TLS termination through Nginx
  • persistent PostgreSQL data outside the containers
  • persistent OpenProject assets outside the containers
  • secrets stored in a protected .env file
  • pinned image tags instead of uncontrolled floating tags

5. Requirements

The deployment requires a Linux server with root or sudo access. The DNS record for projects.example.org should already point to the server. Ports 80 and 443 must be reachable from the outside so HTTP redirects and TLS certificate issuance can work.

Required components:

  • Docker Compose or Podman Compose
  • Nginx
  • Certbot
  • OpenSSL
  • enough disk space for database, assets and backups
  • working DNS resolution
  • an SMTP relay for production email delivery

This article uses Podman Compose. If you prefer Docker, the Compose file is largely the same and the commands can be adapted to docker compose.

6. Installing Host Packages

First, install the required host packages. Package names vary slightly between distributions.

Debian-Based Systems

bash
apt update
apt upgrade -y
apt install -y podman podman-compose nginx certbot python3-certbot-nginx openssl uidmap curl

Red Hat-Based Systems

bash
dnf update -y
dnf install -y podman podman-compose nginx certbot python3-certbot-nginx openssl policycoreutils-python-utils curl

SUSE-Based Systems

bash
zypper refresh
zypper update -y
zypper install -y podman podman-compose nginx certbot python3-certbot-nginx openssl curl

After the installation, verify that Podman and Compose are available.

bash
podman --version
podman-compose --version
nginx -v

7. Preparing the User and Directory Layout

OpenProject gets its own technical user. The application itself runs in containers, but Compose files, assets, backups and database directories should be stored in a clear structure on the host.

bash
groupadd --system openproject
useradd \
--system \
--home-dir /srv/openproject \
--shell /sbin/nologin \
--gid openproject \
openproject

The directories are separated by Compose project, assets, PostgreSQL data and backups.

containerfile
install -d -o openproject -g openproject -m 0755 /srv/openproject
install -d -o openproject -g openproject -m 0755 /srv/openproject/httpdocs
install -d -o openproject -g openproject -m 0750 /srv/openproject/logs
install -d -o openproject -g openproject -m 0750 /srv/openproject/scripts
install -d -o openproject -g openproject -m 0750 /srv/openproject/tmp
install -d -o openproject -g openproject -m 0755 /var/db/openproject
install -d -o openproject -g openproject -m 0755 /var/db/openproject/assets
install -d -o openproject -g openproject -m 0750 /var/db/openproject/backups
install -d -o openproject -g openproject -m 0755 /var/lib/postgresql/openproject

On SELinux systems, make sure the containers are allowed to access the persistent directories.

bash
semanage fcontext -a -t container_file_t '/var/db/openproject(/.*)?'
semanage fcontext -a -t container_file_t '/var/lib/postgresql/openproject(/.*)?'
restorecon -RFv /var/db/openproject /var/lib/postgresql/openproject


8. Generating Secrets

OpenProject needs several secrets: SECRET_KEY_BASE, the PostgreSQL password and, if collaborative editing is enabled, a Hocuspocus secret.

containerfile
install -d -o root -g openproject -m 0750 /srv/openproject/httpdocs
SECRET_KEY_BASE="$(openssl rand -hex 64)"
POSTGRES_PASSWORD="$(openssl rand -hex 24)"
COLLABORATIVE_SERVER_SECRET="$(openssl rand -hex 32)"
ADMIN_BOOTSTRAP_PASSWORD="$(openssl rand -hex 18)"

These values are stored in the .env file in the next step. The file must be protected because it contains application keys and database passwords.

9. Creating the Environment File

The .env file contains image tags, database settings, host names, cache configuration, Hocuspocus settings and the initial administrator account.

bash
cat > /srv/openproject/httpdocs/.env <<EOF
TAG=17-slim
HOCUSPOCUS_TAG=17.4.0
POSTGRES_VERSION=17
POSTGRES_DB=openproject
POSTGRES_USER=openproject
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
SECRET_KEY_BASE=${SECRET_KEY_BASE}
DATABASE_URL=postgres://openproject:${POSTGRES_PASSWORD}@db/openproject?pool=20&encoding=unicode&reconnect=true
OPENPROJECT_HOST__NAME=projects.example.org
OPENPROJECT_HTTPS=true
OPENPROJECT_HSTS=true
OPENPROJECT_DEFAULT__LANGUAGE=de
OPENPROJECT_RAILS__CACHE__STORE=memcache
OPENPROJECT_CACHE__MEMCACHE__SERVER=cache:11211
OPENPROJECT_SEED__ADMIN__USER__NAME=Anna Berger
OPENPROJECT_SEED__ADMIN__USER__MAIL=anna.berger@example.org
OPENPROJECT_SEED__ADMIN__USER__PASSWORD=${ADMIN_BOOTSTRAP_PASSWORD}
OPENPROJECT_SEED__ADMIN__USER__PASSWORD__RESET=true
OPENPROJECT_SEED__ADMIN__USER__LOCKED=false
COLLABORATIVE_SERVER_SECRET=${COLLABORATIVE_SERVER_SECRET}
COLLABORATIVE_SERVER_URL=wss://projects.example.org/hocuspocus
OPENPROJECT_COLLABORATIVE__EDITING__HOCUSPOCUS__URL=wss://projects.example.org/hocuspocus
OPENPROJECT_COLLABORATIVE__EDITING__HOCUSPOCUS__SECRET=${COLLABORATIVE_SERVER_SECRET}
OPENPROJECT_INTERNAL_URL=http://web:8080
EOF
chown root:openproject /srv/openproject/httpdocs/.env
chmod 0600 /srv/openproject/httpdocs/.env

The initial OpenProject administrator login is often admin. The seed variables define the display name, email address and initial password. The password should be changed during the first login.

10. Creating the Compose File

The Compose file separates OpenProject into Web, Worker, Cron and Seeder services. PostgreSQL, Memcached and optional Hocuspocus are added as separate services.

containerfile
name: openproject
networks:
backend:
x-op-restart-policy: &restart_policy
restart: unless-stopped
x-op-image: &image
image: "docker.io/openproject/openproject:${TAG:-17-slim}"
x-op-app: &app
<<: [*image, *restart_policy]
env_file:
- .env
volumes:
- "/var/db/openproject/assets:/var/openproject/assets:Z,U"
networks:
- backend
services:
db:
image: "docker.io/library/postgres:${POSTGRES_VERSION:-17}"
<<: *restart_policy
stop_grace_period: "3s"
volumes:
- "/var/lib/postgresql/openproject:/var/lib/postgresql/data:Z,U"
environment:
POSTGRES_DB: "${POSTGRES_DB:-openproject}"
POSTGRES_USER: "${POSTGRES_USER:-openproject}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
networks:
- backend
cache:
image: "docker.io/library/memcached:1.6"
<<: *restart_policy
networks:
- backend
web:
<<: *app
command: "./docker/prod/web"
hostname: "${OPENPROJECT_HOST__NAME:-localhost:8080}"
ports:
- "127.0.0.1:6000:8080"
depends_on:
- db
- cache
- seeder
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health_checks/default"]
interval: 10s
timeout: 3s
retries: 3
start_period: 30s
worker:
<<: *app
command: "./docker/prod/worker"
depends_on:
- db
- cache
- seeder
cron:
<<: *app
command: "./docker/prod/cron"
depends_on:
- db
- cache
- seeder
seeder:
<<: *app
command: "./docker/prod/seeder"
restart: on-failure
hocuspocus:
image: "docker.io/openproject/hocuspocus:${HOCUSPOCUS_TAG:-17.4.0}"
<<: *restart_policy
ports:
- "127.0.0.1:6001:1234"
environment:
SECRET: "${COLLABORATIVE_SERVER_SECRET}"
OPENPROJECT_URL: "${OPENPROJECT_INTERNAL_URL:-http://web:8080}"
OPENPROJECT_HTTPS: "${OPENPROJECT_HTTPS:-true}"
networks:
- backend

Save the file as /srv/openproject/httpdocs/docker-compose.yaml.

bash
editor /srv/openproject/httpdocs/docker-compose.yaml
chown root:openproject /srv/openproject/httpdocs/docker-compose.yaml
chmod 0644 /srv/openproject/httpdocs/docker-compose.yaml

11. Starting OpenProject Manually

Start the stack manually first before adding a systemd service.

bash
cd /srv/openproject/httpdocs
podman-compose --file docker-compose.yaml --env-file .env up -d

Check the status and logs:

bash
podman ps
podman-compose --file docker-compose.yaml --env-file .env logs -f web
podman-compose --file docker-compose.yaml --env-file .env logs -f seeder

During the first start, the seeder initializes the database. Depending on system performance, this can take a few minutes.

12. Adding a systemd Service

For production use, the Compose stack should be managed through systemd.

bash
[Unit]
Description=OpenProject Podman Compose stack
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/openproject/httpdocs
TimeoutStartSec=900
TimeoutStopSec=240
ExecStart=/usr/bin/podman-compose --file /srv/openproject/httpdocs/docker-compose.yaml --env-file /srv/openproject/httpdocs/.env up -d --remove-orphans
ExecStop=/usr/bin/podman-compose --file /srv/openproject/httpdocs/docker-compose.yaml --env-file /srv/openproject/httpdocs/.env down
ExecReload=/usr/bin/podman-compose --file /srv/openproject/httpdocs/docker-compose.yaml --env-file /srv/openproject/httpdocs/.env up -d --remove-orphans
[Install]
WantedBy=multi-user.target
bash
editor /etc/systemd/system/openproject.service
systemctl daemon-reload
systemctl enable --now openproject
systemctl status openproject --no-pager

13. Configuring the Nginx Reverse Proxy

OpenProject remains reachable only locally. Nginx handles HTTPS, redirects, proxy headers, upload limits and optional WebSocket proxying for Hocuspocus.

bash
upstream backend.projects.example.org {
server 127.0.0.1:6000;
keepalive 64;
}
upstream hocuspocus.projects.example.org {
server 127.0.0.1:6001;
keepalive 16;
}
server {
listen 80;
listen [::]:80;
server_name projects.example.org;
client_max_body_size 128m;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name projects.example.org;
server_tokens off;
client_max_body_size 128m;
ssl_certificate /etc/letsencrypt/live/projects.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/projects.example.org/privkey.pem;
location /hocuspocus {
proxy_http_version 1.1;
proxy_buffering off;
proxy_request_buffering off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Ssl on;
proxy_redirect off;
proxy_pass http://hocuspocus.projects.example.org;
}
location / {
proxy_http_version 1.1;
proxy_buffering off;
proxy_request_buffering off;
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Ssl on;
proxy_redirect off;
proxy_pass http://backend.projects.example.org;
}
}
bash
editor /etc/nginx/sites-available/projects.example.org
ln -sfn /etc/nginx/sites-available/projects.example.org /etc/nginx/sites-enabled/projects.example.org
nginx -t
systemctl reload nginx

14. Enabling TLS

Once DNS points to the server, issue the TLS certificate.

bash
certbot --nginx \
-d projects.example.org \
--redirect \
--agree-tos \
--email admin@example.org

Anschließend wird geprüft, ob OpenProject über HTTPS antwortet.

bash
curl -I https://projects.example.org/


15. First Login and Administrator Account

After startup, OpenProject is available through the public URL:

bash
https://projects.example.org

For many setups, the initial administrator login is admin. The initial password is stored in the .env file as OPENPROJECT_SEED__ADMIN__USER__PASSWORD.

Example values for the article:

  • Administrator: Anna Berger
  • Email: anna.berger@example.org
  • Project manager: Max Schneider
  • Email: max.schneider@example.org
  • Example project: Intranet Relaunch

Screenshots:





16. Creating an Example Project

After the technical installation, create an example project to test work packages, members and roles.

Example structure:

bash
Intranet Relaunch
├── Concept
├── Design
├── Implementation
├── Testing
└── Go-live

Example roles:

  • Anna Berger: system administrator
  • Max Schneider: project manager
  • Lena Fischer: editor
  • David Wagner: developer

This allows you to verify that projects, work packages, roles, notifications and permissions work as expected.

17. Backups

For production environments, at least three areas must be backed up:

  • PostgreSQL database
  • OpenProject assets
  • .env file with secrets

A simple database backup can be created like this:

bash
cd /srv/openproject/httpdocs
podman-compose --file docker-compose.yaml --env-file .env exec -T db \
pg_dump -U openproject openproject > /var/db/openproject/backups/openproject.sql

Assets and configuration are backed up separately:

bash
tar -czf /var/db/openproject/backups/openproject-assets.tar.gz /var/db/openproject/assets
cp /srv/openproject/httpdocs/.env /var/db/openproject/backups/openproject.env
chmod 0600 /var/db/openproject/backups/openproject.env

Backups should be automated and restore tests should be performed regularly.

18. Updates

For updates, adjust the image tag deliberately. Production systems should not rely on uncontrolled floating tags.

bash
cd /srv/openproject/httpdocs
sed -i 's/^TAG=.*/TAG=17-slim/' .env
podman-compose --file docker-compose.yaml --env-file .env pull
systemctl reload openproject

Before updating, back up the database and assets. Also check the release notes for migration-related instructions.

19. Best Practices for Production Operation

The following principles are useful for stable OpenProject installations:

  • use fixed image tags
  • store PostgreSQL data and assets persistently
  • protect .env with mode 0600
  • bind OpenProject only to localhost
  • expose public access only through Nginx and TLS
  • test backups regularly
  • monitor Web, Worker and Cron separately
  • enable Hocuspocus only when collaborative editing is needed
  • configure SMTP early
  • rate-limit login and password endpoints
  • test updates in a staging environment first

20. Package Installation as an Alternative

The package-based installation with DEB or RPM remains available for selected distributions. It is useful mainly for dedicated servers where OpenProject is mostly the only application and where the OpenProject installer is expected to manage the web server, database and configuration.

For new setups, this option should be considered carefully. OpenProject states that no new package repositories are planned for newer Linux versions and that some future features are intended to be available only for Container-based installations.

If long-term flexibility is important, Docker Compose, Podman Compose or Kubernetes with Helm should be considered as the target architecture.

21. Conclusion

OpenProject can be operated as a self-hosted project management and collaboration platform with a clean and maintainable architecture. A Compose stack with Web, Worker, Cron, Seeder, PostgreSQL, Memcached and optional Hocuspocus provides a transparent foundation for production environments.

Nginx handles public HTTPS access, while the containers remain reachable only locally or internally. Persistent data is stored outside the containers, secrets are managed in a protected .env file and updates are performed through deliberately selected image tags.

This creates a long-term maintainable open-source alternative for project management, roadmaps, task coordination and team collaboration.

22. Outlook

As a next step, this setup can be fully automated. An Ansible role can install host packages, create users and directories, generate secrets, render Compose files, configure systemd, deploy Nginx and enable TLS.

For larger environments, Kubernetes with Helm is also worth considering. It provides a more structured way to handle scaling, rollouts, resource limits and platform integration.