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.
Client / Browser|| HTTPSvNginx Reverse Proxy|| HTTP lokal :6000vOpenProject Web|+--> OpenProject Worker+--> OpenProject Cron+--> Memcached+--> PostgreSQL|| WebSocket /hocuspocusvHocuspocus
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
.envfile - 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
apt updateapt upgrade -yapt install -y podman podman-compose nginx certbot python3-certbot-nginx openssl uidmap curl
Red Hat-Based Systems
dnf update -ydnf install -y podman podman-compose nginx certbot python3-certbot-nginx openssl policycoreutils-python-utils curl
SUSE-Based Systems
zypper refreshzypper update -yzypper install -y podman podman-compose nginx certbot python3-certbot-nginx openssl curl
After the installation, verify that Podman and Compose are available.
podman --versionpodman-compose --versionnginx -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.
groupadd --system openprojectuseradd \--system \--home-dir /srv/openproject \--shell /sbin/nologin \--gid openproject \openproject
The directories are separated by Compose project, assets, PostgreSQL data and backups.
install -d -o openproject -g openproject -m 0755 /srv/openprojectinstall -d -o openproject -g openproject -m 0755 /srv/openproject/httpdocsinstall -d -o openproject -g openproject -m 0750 /srv/openproject/logsinstall -d -o openproject -g openproject -m 0750 /srv/openproject/scriptsinstall -d -o openproject -g openproject -m 0750 /srv/openproject/tmpinstall -d -o openproject -g openproject -m 0755 /var/db/openprojectinstall -d -o openproject -g openproject -m 0755 /var/db/openproject/assetsinstall -d -o openproject -g openproject -m 0750 /var/db/openproject/backupsinstall -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.
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.
install -d -o root -g openproject -m 0750 /srv/openproject/httpdocsSECRET_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.
cat > /srv/openproject/httpdocs/.env <<EOFTAG=17-slimHOCUSPOCUS_TAG=17.4.0POSTGRES_VERSION=17POSTGRES_DB=openprojectPOSTGRES_USER=openprojectPOSTGRES_PASSWORD=${POSTGRES_PASSWORD}SECRET_KEY_BASE=${SECRET_KEY_BASE}DATABASE_URL=postgres://openproject:${POSTGRES_PASSWORD}@db/openproject?pool=20&encoding=unicode&reconnect=trueOPENPROJECT_HOST__NAME=projects.example.orgOPENPROJECT_HTTPS=trueOPENPROJECT_HSTS=trueOPENPROJECT_DEFAULT__LANGUAGE=deOPENPROJECT_RAILS__CACHE__STORE=memcacheOPENPROJECT_CACHE__MEMCACHE__SERVER=cache:11211OPENPROJECT_SEED__ADMIN__USER__NAME=Anna BergerOPENPROJECT_SEED__ADMIN__USER__MAIL=anna.berger@example.orgOPENPROJECT_SEED__ADMIN__USER__PASSWORD=${ADMIN_BOOTSTRAP_PASSWORD}OPENPROJECT_SEED__ADMIN__USER__PASSWORD__RESET=trueOPENPROJECT_SEED__ADMIN__USER__LOCKED=falseCOLLABORATIVE_SERVER_SECRET=${COLLABORATIVE_SERVER_SECRET}COLLABORATIVE_SERVER_URL=wss://projects.example.org/hocuspocusOPENPROJECT_COLLABORATIVE__EDITING__HOCUSPOCUS__URL=wss://projects.example.org/hocuspocusOPENPROJECT_COLLABORATIVE__EDITING__HOCUSPOCUS__SECRET=${COLLABORATIVE_SERVER_SECRET}OPENPROJECT_INTERNAL_URL=http://web:8080EOFchown root:openproject /srv/openproject/httpdocs/.envchmod 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.
name: openprojectnetworks:backend:x-op-restart-policy: &restart_policyrestart: unless-stoppedx-op-image: &imageimage: "docker.io/openproject/openproject:${TAG:-17-slim}"x-op-app: &app<<: [*image, *restart_policy]env_file:- .envvolumes:- "/var/db/openproject/assets:/var/openproject/assets:Z,U"networks:- backendservices:db:image: "docker.io/library/postgres:${POSTGRES_VERSION:-17}"<<: *restart_policystop_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:- backendcache:image: "docker.io/library/memcached:1.6"<<: *restart_policynetworks:- backendweb:<<: *appcommand: "./docker/prod/web"hostname: "${OPENPROJECT_HOST__NAME:-localhost:8080}"ports:- "127.0.0.1:6000:8080"depends_on:- db- cache- seederhealthcheck:test: ["CMD", "curl", "-f", "http://localhost:8080/health_checks/default"]interval: 10stimeout: 3sretries: 3start_period: 30sworker:<<: *appcommand: "./docker/prod/worker"depends_on:- db- cache- seedercron:<<: *appcommand: "./docker/prod/cron"depends_on:- db- cache- seederseeder:<<: *appcommand: "./docker/prod/seeder"restart: on-failurehocuspocus:image: "docker.io/openproject/hocuspocus:${HOCUSPOCUS_TAG:-17.4.0}"<<: *restart_policyports:- "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.
editor /srv/openproject/httpdocs/docker-compose.yamlchown root:openproject /srv/openproject/httpdocs/docker-compose.yamlchmod 0644 /srv/openproject/httpdocs/docker-compose.yaml
11. Starting OpenProject Manually
Start the stack manually first before adding a systemd service.
cd /srv/openproject/httpdocspodman-compose --file docker-compose.yaml --env-file .env up -d
Check the status and logs:
podman pspodman-compose --file docker-compose.yaml --env-file .env logs -f webpodman-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.
[Unit]Description=OpenProject Podman Compose stackWants=network-online.targetAfter=network-online.target[Service]Type=oneshotRemainAfterExit=yesWorkingDirectory=/srv/openproject/httpdocsTimeoutStartSec=900TimeoutStopSec=240ExecStart=/usr/bin/podman-compose --file /srv/openproject/httpdocs/docker-compose.yaml --env-file /srv/openproject/httpdocs/.env up -d --remove-orphansExecStop=/usr/bin/podman-compose --file /srv/openproject/httpdocs/docker-compose.yaml --env-file /srv/openproject/httpdocs/.env downExecReload=/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
editor /etc/systemd/system/openproject.servicesystemctl daemon-reloadsystemctl enable --now openprojectsystemctl 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.
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;}}
editor /etc/nginx/sites-available/projects.example.orgln -sfn /etc/nginx/sites-available/projects.example.org /etc/nginx/sites-enabled/projects.example.orgnginx -tsystemctl reload nginx
14. Enabling TLS
Once DNS points to the server, issue the TLS certificate.
certbot --nginx \-d projects.example.org \--redirect \--agree-tos \--email admin@example.org
Anschließend wird geprüft, ob OpenProject über HTTPS antwortet.
curl -I https://projects.example.org/
15. First Login and Administrator Account
After startup, OpenProject is available through the public URL:
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:
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
.envfile with secrets
A simple database backup can be created like this:
cd /srv/openproject/httpdocspodman-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:
tar -czf /var/db/openproject/backups/openproject-assets.tar.gz /var/db/openproject/assetscp /srv/openproject/httpdocs/.env /var/db/openproject/backups/openproject.envchmod 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.
cd /srv/openproject/httpdocssed -i 's/^TAG=.*/TAG=17-slim/' .envpodman-compose --file docker-compose.yaml --env-file .env pullsystemctl 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
.envwith mode0600 - 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.



