18 — OpenProject (Project Management)¶
A self-hosted, FOSS project-management tool that teaches the same WBS + Gantt + baseline + EVM workflow as Microsoft Project and Primavera P6 — the two tools utility, power, and EPC employers actually list in job postings — while also covering the agile / Scrum side that PMP-Agile and software-industry Jira shops expect. Used here as a learning platform for the PMP certification path and as a portfolio artifact for project work.
Companion to 17-caddy.md (the HTTPS front-end), 05-backups.md (Postgres dumped nightly to
backups/+ the assets/config trees rsync'd to/mnt/backupand the off-site T5), and 13-homepage.md (launcher tile).
Overview¶
Multi-container Docker stack — the heaviest single service on the box. Cloned from the upstream opf/openproject-deploy repo (stable/17 branch) verbatim; the only customizations live in .env.
| Container | Image | Purpose |
|---|---|---|
openproject-web-1 |
openproject/openproject:17-slim |
Rails web app (Puma, 4–16 threads) — UI + REST API + Hocuspocus client |
openproject-worker-1 |
openproject/openproject:17-slim |
Background job worker (mail, exports, search index, notifications) |
openproject-cron-1 |
openproject/openproject:17-slim |
Scheduled-jobs runner (daily/weekly maintenance, IMAP poll if enabled) |
openproject-seeder-1 |
openproject/openproject:17-slim |
Runs db:migrate + db:seed on every up, then exits (restart: on-failure) |
openproject-db-1 |
postgres:17 |
Primary database |
openproject-cache-1 |
memcached |
Rails cache store (OPENPROJECT_RAILS__CACHE__STORE=memcache) |
openproject-proxy-1 |
openproject/proxy (built locally from upstream ./proxy/) |
Internal nginx/Caddy that handles X-Accel-Redirect-style attachment streaming and publishes the stack on 127.0.0.1:8082 |
openproject-hocuspocus-1 |
openproject/hocuspocus:17.4.0 |
WebSocket server for real-time collaborative editing of work-package descriptions |
openproject-autoheal-1 |
willfarrell/autoheal:1.2.0 |
Restarts the web container if its health_checks/default endpoint goes unhealthy. Mounts /var/run/docker.sock RW (see Design decisions). |
Reachable at https://openproject.z2mini.gabrielgabrie.com from any tailnet device. The published 127.0.0.1:8082 endpoint is the internal proxy; nothing reaches it except Caddy on the host. Not exposed to the public internet.
Memory: ~1.5–2 GB at idle (Rails + Postgres + memcached + worker + cron + seeder one-shot + hocuspocus + autoheal + proxy). Plenty of headroom on the 64 GB box.
Design decisions¶
| Decision | Reasoning |
|---|---|
| OpenProject over the lighter Trello-clones (Planka, Vikunja, Kanboard) | The lighter tools teach Trello mechanics only. OpenProject's WBS + Gantt + baselines + EVM are the same mental model as MS Project and Primavera P6 — the actual PMBOK / PMP toolkit, and what utility / power / EPC job postings list. Agile boards + sprints are also in the same tool, so the Jira-Scrum muscle memory transfers too. The 5–10× RAM cost vs. Planka is the price of teaching the right thing. |
Upstream opf/openproject-deploy repo cloned verbatim (no compose edits) |
Per the "use upstream config verbatim" rule — the upstream compose has a locally-built proxy/ build context and a fast-moving service graph; hand-typing a stripped-down version is the failure mode. All customizations are in .env (port, hostname, secrets, bind-mount paths). git pull on the stable/17 branch is the upgrade path. |
stable/17 branch + TAG=17-slim |
The current released line as of install (May 2026). -slim ships without the bundled internal nginx (handled by the separate proxy container) and without pdftools/LDAP libs — smaller image, faster pull. Upgrades to stable/18 etc. are deliberate: git pull && docker compose up -d --build --pull always. |
Postgres 17 (the upstream .env.example default for new installs) |
The upstream compose still defaults POSTGRES_VERSION to 13 in its YAML to avoid breaking existing setups; new installs (us) override to 17 via .env. Avoids a future migration. |
Named volumes overridden to bind mounts via PGDATA / OPDATA env vars |
The default named volumes (pgdata, opdata) end up under /var/lib/docker/volumes/ (OS drive) and are awkward to back up. Setting PGDATA=/data/docker/openproject/postgres and OPDATA=/data/docker/openproject/assets in .env swaps them for bind mounts under /data/docker/openproject/ — same pattern as Immich's library/ and postgres/. assets/ stays gabriel-owned (uid 1000 — the container runs as 1000 too); postgres/ is mode 700 uid 999 (Postgres container's user). |
Port 8082 (bound to 127.0.0.1 only), fronted by Caddy |
Free in the reserved-ports table; sits between Vaultwarden (8080) and Beszel (8090). The published port is on the internal proxy container, which proxies to web:8080 over the frontend Docker network. Nothing reaches the container except Caddy, which terminates TLS at https://openproject.z2mini.gabrielgabrie.com. |
OPENPROJECT_HTTPS=true + OPENPROJECT_HOST__NAME=openproject.z2mini.gabrielgabrie.com |
Tells the Rails app to emit https://… URLs in mail, exports, and link previews — required when a TLS-terminating proxy (Caddy) is in front. The HOST__NAME is also the Rails HostAuthorization allow-list — direct requests to 127.0.0.1:8082 with the wrong Host: header get a 400. That's the gate working; smoke-test from the box uses curl -H "Host: openproject.z2mini.gabrielgabrie.com" -H "X-Forwarded-Proto: https" http://127.0.0.1:8082/. |
WebSocket (/hocuspocus) via Caddy's auto-WebSocket upgrade |
Caddy's reverse_proxy upgrades WebSocket connections automatically — no extra config in the site block. COLLABORATIVE_SERVER_URL=wss://openproject.z2mini.gabrielgabrie.com/hocuspocus in .env tells the Rails app to point browsers at the WSS endpoint (same hostname, same cert, port 443). Same pattern Immich and Vaultwarden use for their WebSockets. |
autoheal kept (upstream verbatim) — Docker socket mounted RW |
The trade-off: a compromised autoheal container is root-on-host via the socket. Accepted because (a) the box is single-user, tailnet-only, no public inbound; (b) OpenProject is the heaviest Rails app on the box and the most likely to go unhealthy under memory pressure or a slow query — auto-restart of an unhealthy web is genuinely useful; © keeping the upstream compose unedited is the documented stance. Reconsider if the socket-RW posture ever conflicts with something else. |
SECRET_KEY_BASE, COLLABORATIVE_SERVER_SECRET, POSTGRES_PASSWORD — randomly generated, mode 600 .env |
All three are load-bearing. Rotating SECRET_KEY_BASE invalidates every session and every 2FA-recovery code (Rails uses it to sign cookies and encrypt the 2FA secret column). POSTGRES_PASSWORD is also embedded in DATABASE_URL in .env — change both lines together. |
RAILS_MIN_THREADS=4 / MAX=16 (upstream defaults kept) |
The 10-core / 20-thread i9-10900K + 64 GB box has more than enough capacity. The defaults are sensible for a single-instance Rails app; tune later only if Puma queue depth shows up in real metrics. |
IMAP_ENABLED=false; SMTP unconfigured |
Single-user setup — no inbound email-to-ticket flow, no need for outbound (no invitations, no notifications-via-email yet). Add later by setting IMAP_* for inbound or EMAIL_DELIVERY_METHOD=smtp + SMTP_* for outbound, with a separate Gmail app password from msmtp's (revocation-independence reasoning, same as Beszel's). |
restart: unless-stopped on every service (upstream default) |
Survives reboots, but docker compose stop actually stops. |
What was considered and rejected¶
- Planka — the closest visual Trello clone (~150 MB RAM). Trivially small, but teaches only kanban — no WBS, no Gantt, no baselines, no EVM. The portfolio story "I used OpenProject's Gantt + baseline workflow, which is the same model as MS Project / P6" is materially stronger than "I used a Trello clone" for utility-industry job hunting.
- Vikunja — tasks-first FOSS PM tool with kanban, list, and Gantt views; very lightweight (~50 MB RAM). Closer to a personal task manager than a project tool; no baseline workflow, no EVM. Fine for to-dos, weak for PMBOK practice.
- Kanboard — rock-solid PHP, tiny, mature, lots of plugins. Same "kanban only, no PMBOK toolkit" problem as Planka.
- WeKan — Meteor + MongoDB. Heaviest of the Trello clones (~500 MB RAM), introduces MongoDB to the stack (everything else here is Postgres or SQLite). Rejected on stack-discipline grounds.
- Focalboard — was a great Trello clone; Mattermost killed the personal-server edition in 2024. Only community forks remain. Not a safe pick.
- Taiga — strong Scrum / Kanban tool, very Jira-like for agile. Doesn't cover the predictive (waterfall / Gantt / baselines) side of PMP. OpenProject does both; Taiga only does the agile half.
- Redmine — old, mature, ubiquitous in legacy enterprise. Has issues + milestones + a basic Gantt, but no baseline workflow, no real EVM, no agile boards. Feels like a 2010s issue tracker; OpenProject feels like a 2020s PM tool.
- OpenProject Cloud (paid) — same software, ~$8/user/month. Pointless on a tailnet box where free self-hosting is the explicit posture and the hardware is sunk cost.
stable/16or earlier —stable/17was current at install (May 2026); no reason to start on an older line.- Dropping the upstream
proxycontainer and pointing Caddy directly atweb:8080— would simplify the stack by one container, but loses the upstreamX-Accel-Redirect-style attachment-streaming optimization and deviates from the "use upstream config verbatim" rule. Trade-off rejected. - Built-in OpenProject SAML / OIDC / LDAP auth —
-slimdeliberately doesn't ship the LDAP libs. Single-user setup uses local accounts; bridge to SSO later if multi-user.
Install¶
Directory layout¶
/data/docker/openproject/ ← `git clone` target — upstream stable/17 branch
├── .env ← mode 600 — version pin + hostname + 3 random secrets + bind-mount paths
├── docker-compose.yml ← upstream verbatim — DO NOT edit; customize via .env
├── docker-compose.control.yml ← upstream — separate compose for `bundle exec rake openproject:reset` ops
├── proxy/ ← upstream build context for the internal nginx/Caddy proxy
│ ├── Dockerfile
│ └── ...
├── control/ ← upstream supplementary scripts
├── README.md / .gitignore / .env.example← upstream — kept for git pull
├── postgres/ ← bind mount — Postgres data dir (mode 700, uid 999) — NEVER filesystem-rsync
├── assets/ ← bind mount — uploaded attachments, exports, themes (uid 1000) — rsync-safe
└── backups/ ← nightly pg_dump output (`openproject-db-YYYY-MM-DD.sql.gz`); last 14 retained
.env¶
.env is the only file edited — every customization lives here, and the upstream docker-compose.yml is untouched. Mode 600.
# OpenProject deployment env. Mode 600. Do NOT commit. Three secrets below are randomly generated.
# Regenerating SECRET_KEY_BASE invalidates all sessions + 2FA recovery codes.
TAG=17-slim
POSTGRES_VERSION=17
# Hostname / TLS — Caddy terminates HTTPS upstream; OpenProject must emit https:// URLs.
OPENPROJECT_HTTPS=true
OPENPROJECT_HOST__NAME=openproject.z2mini.gabrielgabrie.com
PORT=127.0.0.1:8082
OPENPROJECT_RAILS__RELATIVE__URL__ROOT=
IMAP_ENABLED=false
RAILS_MIN_THREADS=4
RAILS_MAX_THREADS=16
# Bind-mount targets — override the upstream named volumes so backups can reach the data.
PGDATA=/data/docker/openproject/postgres
OPDATA=/data/docker/openproject/assets
# Secrets (random, generated at install time — head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)
SECRET_KEY_BASE=<64 random alphanumeric chars>
COLLABORATIVE_SERVER_SECRET=<32 random alphanumeric chars>
POSTGRES_PASSWORD=<32 random alphanumeric chars>
# Postgres connection — password must match POSTGRES_PASSWORD above (embedded in the URL).
DATABASE_URL=postgres://postgres:<same as POSTGRES_PASSWORD>@db/openproject?pool=20&encoding=unicode&reconnect=true
# Hocuspocus collaborative-editing WebSocket — fronted by Caddy (wss://, port 443).
COLLABORATIVE_SERVER_URL=wss://openproject.z2mini.gabrielgabrie.com/hocuspocus
First boot¶
# 1. Clone the upstream repo (verbatim).
git clone --depth=1 --branch=stable/17 https://github.com/opf/openproject-deploy.git /data/docker/openproject
# 2. Bind-mount dirs with correct ownership.
mkdir -p /data/docker/openproject/{postgres,assets,backups}
chmod 700 /data/docker/openproject/postgres
sudo chown -R 999:999 /data/docker/openproject/postgres # matches Postgres container uid
# assets/ + backups/ stay gabriel-owned (uid 1000), matches the web container's run-as user.
# 3. Generate three random secrets and write .env (mode 600). See the .env block above.
chmod 600 /data/docker/openproject/.env
# 4. Validate compose, then pull + build + boot.
cd /data/docker/openproject
docker compose config --quiet
docker compose up -d --build --pull always
# 5. Wait for web to go healthy (seeder runs db:migrate + db:seed first — ~30 s to a few min).
until docker compose ps --format '{{.Service}}={{.Status}}' | grep -q '^web=Up.*(healthy)'; do
docker compose ps --format '{{.Service}}={{.Status}}' | grep -E 'web|seeder'
sleep 10
done
# 6. Smoke-test (use the configured Host header — Rails rejects others with HTTP 400):
curl -sI -H "Host: openproject.z2mini.gabrielgabrie.com" -H "X-Forwarded-Proto: https" http://127.0.0.1:8082/
# expect: HTTP/1.1 302 — Location: https://openproject.z2mini.gabrielgabrie.com/login
Caddy site block¶
Append to /data/docker/caddy/Caddyfile, then validate + reload via the admin API (never restart Caddy — see 17-caddy.md):
docker compose -f /data/docker/caddy/docker-compose.yml exec -T caddy caddy validate --config /etc/caddy/Caddyfile
docker compose -f /data/docker/caddy/docker-compose.yml exec -T caddy caddy reload --config /etc/caddy/Caddyfile
# Confirm the new host is loaded (not just in the Caddyfile but actually in the running config):
docker compose -f /data/docker/caddy/docker-compose.yml exec -T caddy \
wget -qO- http://127.0.0.1:2019/config/apps/http/servers/srv0/routes | tr ',' '\n' | grep openproject
# Wait ~20–60 s for Caddy to obtain the cert via DNS-01 (Cloudflare), then verify Let's Encrypt issued it:
echo | openssl s_client -connect openproject.z2mini.gabrielgabrie.com:443 \
-servername openproject.z2mini.gabrielgabrie.com 2>/dev/null \
| openssl x509 -noout -issuer -subject -dates
# expect: issuer=C=US, O=Let's Encrypt, CN=...
Gotcha: running
caddy reloadonce doesn't always take — the admin API responded but the new site never showed in/config/apps/http/servers/srv0/routes. Re-issuing the same reload command picked it up. If a new site block doesn't appear in the admin API's routes list within a few seconds, reload again.
First login¶
- Open
https://openproject.z2mini.gabrielgabrie.comfrom any tailnet device. - Login with the seeded credentials: username
admin, passwordadmin. - OpenProject forces a password change on first login — set a strong one and store it in Vaultwarden.
- Set the System Administrator email under Administration → System settings → General. Even with SMTP off it's used for reset tokens and for the
from:address if mail is enabled later. - Create your first Project — give it a name + identifier; the identifier becomes the URL slug and is hard to change later.
- Start by adding work packages (WBS) and a Gantt view. Set a baseline on the project before you start tracking variance — that's the workflow that transfers directly to MS Project / Primavera.
Using OpenProject for PMP practice¶
The point of this install is to get reps on the predictive + agile + hybrid PMBOK workflows in a tool whose mental model transfers to MS Project, Primavera P6, and Jira. A starter pattern:
| PMBOK concept | OpenProject feature | What you do |
|---|---|---|
| Work Breakdown Structure | Hierarchical work packages | Project → Work packages → indent children under parents; the indent tree IS the WBS |
| Gantt chart | Work packages → Gantt view | Drag bars to set start/finish; right-click → Add predecessor for FS/SS/FF/SF; lag in days/hours |
| Milestones | Work-package type "Milestone" | Zero-duration markers; flag major handoffs |
| Baseline / variance tracking | Project → Baselines (Enterprise feature in some editions; check yours) | Snapshot the schedule, then track planned vs. actual over time |
| Critical path | Gantt → enable critical-path highlight | Same model as MS Project's "Schedule → Network Diagram → Critical Path" |
| Costs / EVM | Work package → cost / spent units | Enter labor cost types; build PV / EV / AC by phase |
| Risk register | Work-package type "Risk" (or a custom type) | One WP per risk; custom fields for probability × impact |
| Agile / Scrum | Boards → Backlog + Sprint board | Drag stories from backlog into a sprint; burndown auto-tracked |
| Stakeholder mgmt | Members + roles per project | Project → Members; map RACI by role assignment |
The skills transfer story for a job interview:
"I ran my homelab projects in OpenProject — built the WBS, set baselines, tracked work-package variance against schedule, reviewed EVM weekly. The Gantt + baseline workflow is the same model as MS Project and Primavera P6, so I'd ramp on those quickly. I also ran an agile board on a side project to cover the Scrum side of PMP-Agile."
That's a real, defensible PMP-aligned story.
Operations¶
Start / stop / pull updates¶
cd /data/docker/openproject
docker compose up -d # start (or recreate after pull)
docker compose stop # stop without removing
docker compose down # stop + remove containers (data preserved on bind mounts)
# Updates: pull the upstream branch (stable/17), then rebuild proxy + pull base images.
git pull origin stable/17 # may bump TAG default in .env.example
docker compose pull
docker compose up -d --build --pull always # seeder re-runs db:migrate
docker compose logs -f --tail 30 seeder web
Logs¶
docker compose logs -f --tail 50 web # Rails app
docker compose logs -f --tail 50 worker # background jobs
docker compose logs -f --tail 50 seeder # only useful right after up -d
docker compose logs -f --tail 50 db # Postgres
Connecting from on the server itself¶
Direct: http://127.0.0.1:8082/ with Host: openproject.z2mini.gabrielgabrie.com and X-Forwarded-Proto: https headers. Without the right Host: header, Rails rejects with HTTP 400 (the HostAuthorization allow-list).
curl -sI -H "Host: openproject.z2mini.gabrielgabrie.com" -H "X-Forwarded-Proto: https" http://127.0.0.1:8082/
# HTTP/1.1 302 — Location: https://openproject.z2mini.gabrielgabrie.com/login
Via Caddy (works from on-box too): https://openproject.z2mini.gabrielgabrie.com — resolves to 100.67.235.68 (the box's tailnet IP), hits Caddy, no Host: gymnastics needed.
Rails console (rare; for emergency admin)¶
Useful for things like resetting the admin password if you forget it (User.find_by(login: 'admin').update!(password: 'NewPassword!1', password_confirmation: 'NewPassword!1')). Be careful — there's no undo.
Backup considerations¶
In the nightly backup since install. Three pieces:
- Postgres database — captured via
pg_dumpfrom inside thedbcontainer, written to/data/docker/openproject/backups/openproject-db-YYYY-MM-DD.sql.gz(gzipped). Last 14 retained, same retention as Immich's auto-backup. Thebackups/directory is then picked up by the per-service rsync (same pattern Immich uses with itslibrary/backups/). Never raw-rsync thepostgres/data dir — a live Postgres data dir snapshot is corrupt. - Uploaded attachments + exports (
assets/) — files-as-truth, owned1000:1000, rsync-safe. Goes intoservice-config/openproject/assets/. - Config + secrets —
.env(mode 600 — three load-bearing secrets), the upstream compose + proxy build context, the upstream README/control/etc. files. Small, rsync-safe.
What's not backed up:
postgres/— covered by the.sql.gzdump instead (see above).- Docker named volumes — there aren't any; both
pgdataandopdataare overridden to bind mounts under/data/docker/openproject/, so everything is under/data(the data drive) and reachable by the backup user.
On the off-site T5 — service-config/openproject/ is included (the backup-offsite.sh script copies the whole service-config/ tree). That covers the database dump, the assets, and the config — everything needed to restore.
Restore (per 08-recovery.md Step 6b conceptually):
# 1. Re-clone upstream + restore .env + bind-mount dirs:
git clone --depth=1 --branch=stable/17 https://github.com/opf/openproject-deploy.git /data/docker/openproject
rsync -a /mnt/backup/current/service-config/openproject/ /data/docker/openproject/
chmod 600 /data/docker/openproject/.env
sudo chown -R 999:999 /data/docker/openproject/postgres
chmod 700 /data/docker/openproject/postgres
# 2. Boot DB only, then load the dump:
docker compose up -d db
sleep 10
gunzip -c /data/docker/openproject/backups/openproject-db-<DATE>.sql.gz \
| docker compose exec -T db psql -U postgres -d openproject
# 3. Boot the rest.
docker compose up -d
docker compose logs -f --tail 50 web # wait for "Listening on tcp://0.0.0.0:8080"
Troubleshooting¶
HTTP 400 Bad Request directly to 127.0.0.1:8082:
- Rails
HostAuthorizationmiddleware rejects anyHost:header not in the allow-list.127.0.0.1:8082isn't on the list — the only allowed host isOPENPROJECT_HOST__NAME(openproject.z2mini.gabrielgabrie.com). - Smoke-test with the right header:
curl -H "Host: openproject.z2mini.gabrielgabrie.com" -H "X-Forwarded-Proto: https" http://127.0.0.1:8082/. Or just go through Caddy (curl -kI https://openproject.z2mini.gabrielgabrie.com/) — Caddy passes the originalHost:through unchanged.
Web container stuck at "health: starting":
- The seeder may still be running
db:migrateordb:seed. Check:docker compose logs --tail 30 seeder. First-boot seeding takes 30 s to a few minutes; major-version upgrades (e.g.stable/16→stable/17) longer. Web only goes healthy after the seeder exits. - If seeder exits with a non-zero code, check Postgres logs:
docker compose logs db | tail -50. Common cause: stalepostgres/dir from a different Postgres major version — wipe and reseed (docker compose down,sudo rm -rf /data/docker/openproject/postgres/*,docker compose up -d).
Caddy returns 502 / 504:
- Web isn't healthy or isn't listening. Confirm:
docker compose ps web→ "Up X (healthy)" andss -tln | grep 8082→127.0.0.1:8082. The internal proxy container is what binds 8082; ifproxyis down butwebis up, you'll still get 502. - Check Caddy's log for the upstream dial error:
docker compose -f /data/docker/caddy/docker-compose.yml logs --tail 30 caddy | grep -i upstream.
Browser shows "Connection refused" / "DNS rebinding" / can't resolve *.z2mini.gabrielgabrie.com:
- Same DNS story as every other service here — see 17-caddy.md → Troubleshooting. Tailscale must be connected; Tailscale's global resolvers (
1.1.1.1/8.8.8.8) must be on with "Override local DNS".
Hocuspocus WebSocket fails / collaborative editing doesn't connect:
- Browser DevTools → Network → WS — look for a failed
/hocuspocusconnection. The WSS URL iswss://openproject.z2mini.gabrielgabrie.com/hocuspocus; Caddy auto-upgrades to WebSocket, no extra config needed. - Common cause:
COLLABORATIVE_SERVER_URLin.envis wrong (http://instead ofwss://, wrong hostname). Re-check.env, thendocker compose up -d web hocuspocus. - Less commonly:
COLLABORATIVE_SERVER_SECRETmismatch between.envand a stale hocuspocus container —docker compose restart hocuspocus.
docker compose up -d re-runs the seeder every time and it takes 30+ s:
- Expected — the seeder runs
db:migrateon every up. It's a no-op when there are no pending migrations (just a few seconds of Rails boot + version check). Therestart: on-failurepolicy means it doesn't loop unless it actually fails.
Forgot the admin password:
docker compose exec web bundle exec rails runner \
"u = User.find_by(login: 'admin'); u.password = 'NewPassword!1'; u.password_confirmation = 'NewPassword!1'; u.save!"
Then log in with admin / NewPassword!1 and change it via the UI.
Autoheal restarted web unexpectedly:
- Check
web's logs around the autoheal event for the underlying unhealthy cause:docker compose logs --tail 200 web. Look for OOM kills (Killed), database connection-pool exhaustion, or Hocuspocus link failures. Autoheal restarting is the symptom, not the bug.
See also¶
- 17-caddy.md — the HTTPS front-end at
https://openproject.z2mini.gabrielgabrie.com - 05-backups.md — nightly
pg_dump+ per-service-config rsync; alsobackup-offsite.shfor the off-site T5 - 13-homepage.md — the launcher tile pointing at the OpenProject UI
- 10-system-reference.md — quick lookup: port
8082, container names, paths - z2mini-context-for-ai.md — AI-context document, kept in sync with this page
- Upstream:
opf/openproject-deploystable/17— the compose this install was cloned from - Upstream: OpenProject configuration reference — every
OPENPROJECT_*env var