Andrew Mercer

OpenProject — Homelab Deployment

Self-hosted OpenProject instance running via Docker Compose, backed by a bare-metal
PostgreSQL instance at [ ip_address ], with outbound mail relayed through the internal
Postfix container.

  • URL: https://[ hostname ]
  • Container name: openproject
  • Image: openproject/openproject:15
  • Network: internal_lab_net
  • Database: PostgreSQL (bare metal, [ ip_address ])
  • Mail relay: Postfix container (postfix), no auth required internally

Directory layout

project_management/openproject/
├── [ hostname ]
├── .env                  # not committed — real secrets live here
└── [ hostname ]

Prerequisites

  • Docker + Docker Compose installed on lab
  • Network internal_lab_net already created (docker network ls to confirm)
  • Reachable Postgres instance at [ ip_address ] with a database and user provisioned
    (see Creating the database below)
  • Postfix container running and reachable on the same Docker network

[ hostname ]

services:
  openproject:
    image: openproject/openproject:15
    container_name: openproject
    restart: unless-stopped
    ports:
      - "8090:80"
    volumes:
      - openproject_data:/var/openproject/assets
    env_file:
      - .env
    networks:
      - default

networks:
  default:
    name: ${NETWORK:-internal_lab_net}
    external: true

volumes:
  openproject_data:
    external: true
    name: openproject_openproject_data

.env

Not committed to git. Create it manually in this directory:

NETWORK=internal_lab_net
OPENPROJECT_HOST__NAME=[ hostname ]
OPENPROJECT_HTTPS=true
OPENPROJECT_SECRET_KEY_BASE=<generate with: openssl rand -hex 64>

# NOTE: DATABASE_URL must have the password inlined directly.
# .env files do not expand ${VAR} references to other lines in the same file —
# referencing ${POSTGRES_PASSWORD} here will resolve to blank.
DATABASE_URL=postgresql://openproject:<actual-password>@[ ip_address ]/openproject

OPENPROJECT_EMAIL__DELIVERY__METHOD=smtp
OPENPROJECT_SMTP__ADDRESS=postfix
OPENPROJECT_SMTP__PORT=25
OPENPROJECT_SMTP__DOMAIN=[ hostname ]
OPENPROJECT_SMTP__AUTHENTICATION=none
OPENPROJECT_SMTP__ENABLE__STARTTLS__AUTO=true
OPENPROJECT_SMTP__OPENSSL__VERIFY__MODE=none
OPENPROJECT_RAILS__CACHE__STORE=memcache
chmod 600 .env

Add to .gitignore:

.env

Any env var prefixed OPENPROJECT_SMTP__* or OPENPROJECT_EMAIL__* locks the
corresponding field in the admin UI (Administration → Emails and notifications) as
read-only. To change mail settings, edit .env and recreate the container — the UI
won't accept changes while these are env-managed.


Creating the database

Run on the Postgres host itself ([ ip_address ]) via the local socket — this avoids
pg_hba.conf restrictions on remote superuser connections:

ssh [ ip_address ]
sudo -u postgres psql

Inside psql:

CREATE USER openproject WITH PASSWORD 'REPLACE_WITH_GENERATED_PASSWORD';
CREATE DATABASE openproject OWNER openproject ENCODING 'UTF8';
GRANT ALL PRIVILEGES ON DATABASE openproject TO openproject;

Generate a strong password rather than typing one from memory:

openssl rand -hex 24

Exit psql with \q.


Connecting to / navigating the database

From the Postgres host directly:

sudo -u postgres psql -d openproject

Remotely (requires a matching pg_hba.conf entry for your source IP/subnet):

psql -h [ ip_address ] -U openproject -d openproject

From inside the running OpenProject container (uses the connection string already in
its environment — no need to re-enter credentials):

docker exec -it openproject psql "$DATABASE_URL"

Useful psql commands once connected:

Command Purpose
\l List all databases
\dt List tables in current database
\d work_packages Describe a specific table's schema
\du List roles/users
\q Quit

Quick sanity checks:

-- row count on a core table
SELECT count(*) FROM work_packages;

-- database size on disk
SELECT pg_size_pretty(pg_database_size('openproject'));

Changing the database password

  1. Generate a new password:
    bash openssl rand -hex 24

  2. Apply it at the database (on the Postgres host, via socket):
    bash ssh [ ip_address ] sudo -u postgres psql -c "ALTER USER openproject WITH PASSWORD 'NEW_PASSWORD';" exit
    Takes effect immediately — no Postgres restart needed.

  3. Update DATABASE_URL in .env with the new password (inlined, not referenced).

  4. Recreate the container so it picks up the new value:
    bash docker compose up -d


Changing the OpenProject secret key base

Only rotate this if it's been exposed or as part of a periodic rotation — rotating it
invalidates all active sessions (users get logged out once) and, depending on version,
may affect any data encrypted using it.

openssl rand -hex 64

Update OPENPROJECT_SECRET_KEY_BASE in .env, then:

docker compose up -d

Starting, stopping, recreating

docker compose up -d          # start / recreate with current .env and [ hostname ]
docker compose down           # stop and remove the container (volumes persist — declared external)
docker compose logs -f openproject   # tail logs
docker compose restart openproject   # restart without recreating

Since both openproject_data and the Postgres database are external to this compose
project, docker compose down never destroys data — only the container itself.


Backups

Application assets and the database should be backed up separately:

  • Database: pg_dump against [ ip_address ], or the db-backup Rust tool if set up
    (see homelab tooling repo).
    bash docker exec openproject bash -c 'pg_dump "$DATABASE_URL" -F c -f /tmp/[ hostname ]' docker cp openproject:/tmp/[ hostname ] ~/openproject-backup-$(date +%Y%m%d).dump

  • Assets volume (openproject_openproject_data):
    bash docker run --rm \ -v openproject_openproject_data:/data \ -v ~/openproject-volume-backups:/backup \ alpine tar czf /backup/openproject-assets-$(date +%Y%m%d).[ hostname ] -C /data .


Troubleshooting

"Invalid delivery method :letter_opener_web"
An OPENPROJECT_EMAIL__DELIVERY__METHOD env var is stuck on the dev/test value.
Env vars always override [ hostname ] — edit .env and recreate the container;
editing config files inside a running container has no effect.

"certificate verify failed (hostname mismatch)" on SMTP
Postfix's TLS cert doesn't match the hostname OpenProject is connecting with. Either
connect using the hostname the cert was actually issued for, or (simplest, for
internal-only traffic) disable verification:

OPENPROJECT_SMTP__OPENSSL__VERIFY__MODE=none

POSTGRES_PASSWORD variable is not set. Defaulting to a blank string.
Caused by referencing ${POSTGRES_PASSWORD} inside DATABASE_URL within the same
.env file — this doesn't get expanded. Inline the real password directly into
DATABASE_URL instead of referencing another .env variable.

Mail settings fields greyed out in the admin UI
Expected behavior — any SMTP/email setting supplied via environment variable becomes
read-only in the UI. Change it in .env and run docker compose up -d.

no pg_hba.conf entry for host ... no encryption
Your client IP isn't permitted to connect remotely. Either run psql directly on the
Postgres host via the Unix socket (sudo -u postgres psql), or add an entry to
pg_hba.conf for your subnet and systemctl reload postgresql.

Lost [ hostname ] / can't find where a running container's config lives

docker inspect <container> --format '{{ index .[ hostname ] "[ hostname ].working_dir" }}'
docker inspect <container> --format '{{ index .[ hostname ] "[ hostname ].config_files" }}'

If the directory no longer exists, reconstruct [ hostname ]/.env from the container's
live environment (docker exec <container> env) and its mounted volume names
(docker inspect <container> --format '{{ range .Mounts }}{{ .Name }} -> {{ .Destination }}{{ "\n" }}{{ end }}'),
then recreate — it will reattach to existing external volumes/database rather than
starting fresh, as long as the volume names and DATABASE_URL match what's already there.


Security notes

  • .env contains live credentials — chmod 600, never commit it, never paste its full
    contents into chat/tickets/logs.
  • If a secret is ever exposed (chat, logs, screenshare, etc.), treat it as compromised
    and rotate it — see "Changing the database password" / "secret key base" above.
  • Long-term: credentials here should move to a proper secrets backend (OpenBao/Vault)
    rather than living in a plaintext .env file at rest.