Post

how to host openproject on a vps with kamal

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_timeout is a root-level key, not nested under proxy:.
  • Accessory hostnames are auto-derived as <service>-<accessory> (e.g. openproject-db, openproject-cache) — that’s why DATABASE_URL references openproject-db.
  • /health_checks/default is 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

SymptomCauseFix
Configuration file not foundConfig in wrong pathMust be config/deploy.yml, not .kamal/deploy.yml
Builder arch not setMissing builder blockAdd builder: { arch: amd64 }
Missing DockerfileTrying to deploy prebuilt image directlyAdd the one-line Dockerfile above
invalid reference formatTag included in image:image: must be bare repo name, no :tag
target failed to become healthy within 30sDefault deploy timeout too shortSet root-level deploy_timeout: 300
password authentication failedPostgres volume initialized with old/mismatched credentialskamal 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 signupSelf-registered instead of using seeded adminLog in as admin / admin (forced password change), then activate/promote your account under Administration → Users
Forgot admin passwordkamal 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
This post is licensed under CC BY-NC 4.0 by the author.