how to host openproject on a vps with kamal
Hosting OpenProject on a VPS with Kamal
Kamal needs to build an image, so you can’t point it directly at openproject/openproject. Wrap it in a one-line Dockerfile instead.
1. Project structure
1
2
3
4
5
6
openproject-deploy/
Dockerfile
config/
deploy.yml
.kamal/
secrets
2. Dockerfile
1
FROM openproject/openproject:15
Kamal will build/retag this and push it to your own Docker Hub namespace.
3. config/deploy.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
service: openproject
image: <your-dockerhub-user>/openproject
deploy_timeout: 300 # first boot runs migrations, default 30s is too short
builder:
arch: amd64
servers:
web:
hosts:
- <VPS_IP>
proxy:
ssl: true
host: openproject.yourdomain.com
app_port: 8080
healthcheck:
path: /health_checks/default
interval: 5
timeout: 300
registry:
server: docker.io
username: <your-dockerhub-user>
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
OPENPROJECT_HTTPS: "true"
OPENPROJECT_HOST__NAME: openproject.yourdomain.com
RAILS_CACHE_STORE: memcache
OPENPROJECT_CACHE__MEMCACHE__SERVER: openproject-cache:11211
secret:
- DATABASE_URL
- OPENPROJECT_SECRET_KEY_BASE
volumes:
- "openproject_assets:/var/openproject/assets"
accessories:
db:
image: postgres:15
host: <VPS_IP>
port: "127.0.0.1:5433:5432"
env:
clear:
POSTGRES_DB: openproject
secret:
- POSTGRES_USER
- POSTGRES_PASSWORD
volumes:
- "openproject_pg_data:/var/lib/postgresql/data"
cache:
image: memcached:1.6
host: <VPS_IP>
Notes:
deploy_timeoutis a root-level key, not nested underproxy:.- Accessory hostnames are auto-derived as
<service>-<accessory>(e.g.openproject-db,openproject-cache) — that’s whyDATABASE_URLreferencesopenproject-db. /health_checks/defaultis OpenProject’s built-in app health endpoint.
4. .kamal/secrets
Plain environment variables — set these in your shell (or a local .env file you source before running Kamal) rather than pulling from a secrets manager.
1
2
3
4
5
6
7
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
POSTGRES_USER=$OPENPROJECT_POSTGRES_USER
POSTGRES_PASSWORD=$OPENPROJECT_DATABASE_PASSWORD
DATABASE_URL=postgresql://$OPENPROJECT_POSTGRES_USER:$OPENPROJECT_DATABASE_PASSWORD@openproject-db:5432/openproject
OPENPROJECT_SECRET_KEY_BASE=$OPENPROJECT_SECRET_KEY_BASE
Before deploying, export the actual values, e.g.:
1
2
3
4
export KAMAL_REGISTRY_PASSWORD="your-dockerhub-token"
export OPENPROJECT_POSTGRES_USER="openproject"
export OPENPROJECT_DATABASE_PASSWORD="a-strong-password"
export OPENPROJECT_SECRET_KEY_BASE="$(openssl rand -hex 64)"
Critical: the username inside DATABASE_URL must match POSTGRES_USER exactly — reference the same variable, don’t hardcode it. A mismatch causes password authentication failed.
Keep these exports out of version control (e.g. in a gitignored .env file you source .env before running kamal commands).
5. Deploy
1
2
cd openproject-deploy
kamal setup
Watch progress:
1
kamal app logs -f
First boot is slow (migrations + asset precompile) — budget several minutes.
6. Common issues
| Symptom | Cause | Fix |
|---|---|---|
Configuration file not found | Config in wrong path | Must be config/deploy.yml, not .kamal/deploy.yml |
Builder arch not set | Missing builder block | Add builder: { arch: amd64 } |
Missing Dockerfile | Trying to deploy prebuilt image directly | Add the one-line Dockerfile above |
invalid reference format | Tag included in image: | image: must be bare repo name, no :tag |
target failed to become healthy within 30s | Default deploy timeout too short | Set root-level deploy_timeout: 300 |
password authentication failed | Postgres volume initialized with old/mismatched credentials | kamal accessory remove db && docker volume rm openproject_pg_data && kamal accessory boot db — then ensure DATABASE_URL user matches POSTGRES_USER |
| “Account not yet activated” after signup | Self-registered instead of using seeded admin | Log in as admin / admin (forced password change), then activate/promote your account under Administration → Users |
| Forgot admin password | — | kamal app exec -i "bin/rails console" → u = User.find_by(login: "admin"); u.password = u.password_confirmation = "NewPass123!"; u.save! |
7. Useful commands
1
2
3
4
5
kamal app exec -i bash # shell into app container
kamal app exec -i "bin/rails console"
kamal accessory logs db
kamal accessory reboot db
kamal deploy # redeploy after config/image changes