How to Deploy OpenProject on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-host OpenProject Community Edition on a VPS with the official Docker Compose setup — generated secrets, the port kept off the public internet, HTTPS through Caddy, and backups.
- A VPS with at least 4 GB RAM
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A domain or subdomain you can point at the server
- Docker Engine + Compose installed (see the base guide below)
- git
What OpenProject is
OpenProject is open-source software for project, product and portfolio management. The Community Edition covers work packages (tasks, features, milestones and their hierarchy), Gantt charts, agile and kanban boards, Scrum backlogs, time tracking and a project wiki, with TOTP and WebAuthn two-factor authentication included. It is written in Ruby and TypeScript on PostgreSQL, licensed GPL-3.0, and rated 4 / 5 to deploy — the highest of the project-management tools in the catalog, mostly because of its size and its HTTPS assumptions.
LDAP group sync, SSO, resource management and custom branding are Enterprise add-ons and are not part of the free build.
Comparing options? Plane vs OpenProject sets it against the newer, lighter-looking alternative; Deploy Plane on a VPS covers that one.
Server sizing
The Compose setup runs OpenProject's web server, a background worker, a cron container, a one-off seeder that runs migrations, PostgreSQL, memcached, a small proxy, a collaborative-editing server and an autoheal watchdog.
- Minimum: 4 GB RAM — the catalog's floor for OpenProject.
- Measured: on a GCP e2-standard-2 running Ubuntu 26.04 with Docker 29.8.1, the idle stack used ~569 MB of RAM and ~2.7 GB of disk for images and data (September 2026).
The idle figure is low because nobody is using it; Rails workers grow with concurrent users, and the first start (asset setup and migrations) is the heaviest moment. Stay at 4 GB or above, with a 40 GB+ disk for attachments.
Prepare the server
This guide assumes Docker Engine and the Compose plugin are installed, along
with a non-root deploy user and a ufw firewall. If not, work through
Docker & Compose on Ubuntu first.
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo apt-get install -y git
Paid link — we earn a commission if you shop through it.
Install OpenProject (Docker Compose)
Clone upstream's
openproject-docker-compose
repository on the stable/17 branch — the release line the catalog verified —
and start from its example environment file:
cd ~
[ -d openproject ] || git clone https://github.com/opf/openproject-docker-compose.git --depth=1 --branch=stable/17 openproject
cd ~/openproject
[ -f .env ] || cp .env.example .env
The example .env sets TAG=17-slim (the image upstream recommends for this
setup) and OPDATA=/var/openproject/assets, a host directory for attachments
that upstream says to create and hand to UID 1000:
sudo mkdir -p /var/openproject/assets
sudo chown 1000:1000 -R /var/openproject/assets
Configure .env
Four changes, all in .env so they survive git pull:
SECRET_KEY_BASEships asOVERWRITE_ME. It derives the Rails keys; changing it later invalidates every session and remembered 2FA device.COLLABORATIVE_SERVER_SECRETships assecret12345, and upstream's README tells you to override it.- The database password is
p4ssw0rdin bothDATABASE_URLand the compose file'sPOSTGRES_PASSWORDdefault. Set both to the same new value before the first start — Postgres only reads it on initialisation. - The host name and HTTPS:
OPENPROJECT_HOST__NAMEis your domain, andOPENPROJECT_HTTPSgoes back totrue, because this install sits behind Caddy.COLLABORATIVE_SERVER_URLbecomeswss://on that domain.
cd ~/openproject
if grep -q '^SECRET_KEY_BASE=OVERWRITE_ME' .env; then
DB_PW=$(openssl rand -hex 16)
sed -i \
-e "s|^SECRET_KEY_BASE=.*|SECRET_KEY_BASE=$(openssl rand -hex 64)|" \
-e "s|^COLLABORATIVE_SERVER_SECRET=.*|COLLABORATIVE_SERVER_SECRET=$(openssl rand -hex 32)|" \
-e "s|postgres:p4ssw0rd@db|postgres:${DB_PW}@db|" \
.env
echo "POSTGRES_PASSWORD=${DB_PW}" >> .env
fi
sed -i \
-e 's|^OPENPROJECT_HTTPS=.*|OPENPROJECT_HTTPS=true|' \
-e 's|^OPENPROJECT_HOST__NAME=.*|OPENPROJECT_HOST__NAME=op.example.com|' \
-e 's|^COLLABORATIVE_SERVER_URL=.*|COLLABORATIVE_SERVER_URL=wss://op.example.com/hocuspocus|' \
-e 's|^PORT=.*|PORT=127.0.0.1:8080|' \
.env
chmod 600 .env
grep -E '^(TAG|OPENPROJECT_HTTPS|OPENPROJECT_HOST__NAME|PORT|COLLABORATIVE_SERVER_URL)=' .env
Replace op.example.com with your domain. PORT=127.0.0.1:8080 keeps the
stack's proxy on the loopback interface; upstream's README points out that
without the address it binds to 0.0.0.0 and is public.
Start it
cd ~/openproject
docker compose up -d --build --pull always
timeout 900 bash -c 'until curl -fs -H "Host: op.example.com" -H "X-Forwarded-Proto: https" http://127.0.0.1:8080/health_checks/default >/dev/null; do sleep 10; done'
docker compose ps --format '{{.Service}}\t{{.Status}}'
--build builds the small proxy image locally, which is expected: upstream's
troubleshooting notes that a "pull access denied for openproject/proxy"
message during this step is a warning, not a failure. The first start runs the
seeder, which creates the database schema, so the health check can take
several minutes to answer. The two headers in the check stand in for Caddy: with
OPENPROJECT_HTTPS=true, OpenProject expects requests for its own host name
arriving over HTTPS.
HTTPS + domain
Point an A record for op.example.com at the server's public IP, wait for it
to resolve, then terminate TLS with Caddy:
op.example.com {
reverse_proxy 127.0.0.1:8080
}
Caddy sets X-Forwarded-Proto and X-Forwarded-Host, and the stack's own proxy
passes them on to OpenProject, so no extra headers are needed. It also proxies
the WebSocket connection that collaborative editing uses at /hocuspocus.
Without a TLS proxy and with OPENPROJECT_HTTPS=true, browsers end up at an
https:// address nothing is answering — upstream's README describes that as an
ERR_SSL_PROTOCOL_ERROR. OPENPROJECT_HTTPS=false exists only for a first
look without a domain.
First login
Open https://op.example.com and sign in with upstream's default credentials,
admin / admin. OpenProject makes you set a new password on that first
login; do it before anything else, since the default is public knowledge.
Then, under Administration:
- Authentication: decide whether self-registration is allowed at all, and turn on two-factor authentication (TOTP and WebAuthn are in the free edition).
- Email: configure SMTP so notifications and invitations are delivered.
- Users: create accounts or invite your team instead of sharing
admin.
Backups
Two things matter: the PostgreSQL database and the attachments in
/var/openproject/assets.
cd ~/openproject
mkdir -p ~/openproject-backups
docker compose exec -T db pg_dump -U postgres openproject | gzip > ~/openproject-backups/openproject-db-$(date +%F).sql.gz
sudo tar czf ~/openproject-backups/openproject-assets-$(date +%F).tar.gz -C /var/openproject assets
ls -lh ~/openproject-backups
pg_dump gives a consistent snapshot without stopping the stack. Copy the
backups and .env off the server; without SECRET_KEY_BASE a restore
works but logs everyone out and drops remembered 2FA devices.
Upgrades
Minor releases within a line come from pulling new images:
cd ~/openproject
docker compose pull --ignore-buildable
docker compose up -d --build
docker compose ps --format '{{.Service}}\t{{.Status}}'
The seeder runs any migrations on start. Major upgrades (17 to 18, say)
mean switching the repository to the new stable/<n> branch and changing TAG
— back up first and read OpenProject's upgrade notes for that version. The
.env example also warns that POSTGRES_VERSION must match the version your
data directory was created with; upgrading Postgres is a separate
dump-and-restore job.
Troubleshooting
ERR_SSL_PROTOCOL_ERROR or a redirect loop. HTTPS mode and the proxy
disagree. Behind Caddy, keep OPENPROJECT_HTTPS=true and make sure you are
visiting the https:// URL; without a proxy, set it to false.
Links and redirects point at the wrong address. OPENPROJECT_HOST__NAME
doesn't match the domain in your browser. Fix it in .env and run
docker compose up -d.
Attachments fail to save. /var/openproject/assets is not owned by UID
1000. Re-run the chown from the install step.
The web container keeps restarting. Read docker compose logs web seeder.
The autoheal container restarts web when its health check fails, so a
database connection error — often a password changed after the first start —
shows up as a restart loop.
Verification + next steps
You're done when you can: load https://op.example.com over a valid
certificate, log in and replace the default admin password, create a project
with work packages and see them on a Gantt chart and a board, attach a file, and
find last night's dump off the server.
From there: set up SMTP, turn on 2FA for every account, and put
docker-compose.override.yml to use for any change to the compose file itself —
upstream recommends that over editing docker-compose.yml, which git pull
overwrites. For hosting options, see Best VPS for Docker.