Skip to content

Commit a8da511

Browse files
authored
fix(docker): move Postgres to a named volume and bump to Postgres 18 (#486)
* fix(docker): store Postgres cluster in a pgdata subdirectory The prod compose bind-mounted ./docker-data/postgres straight onto /var/lib/postgresql/data. The mount-point root is owned by the host, so Postgres could refuse to (re)initialize on repeated `docker compose up` runs. Point PGDATA at a pgdata/ subdirectory the Postgres entrypoint creates and owns itself, sidestepping the mount-point ownership. Existing deployments must move their cluster into docker-data/postgres/pgdata once before upgrading; documented in the Docker setup guide. * fix(docker): rework Postgres ownership fix as a chown-init sidecar Replace the PGDATA-subdirectory approach with a postgres-init service, mirroring the existing elasticsearch-init pattern: it chowns the bind-mounted ./docker-data/postgres path to postgres:postgres before the postgres service starts. This corrects ownership in place on every `docker compose up`, so existing deployments need no data migration (the PGDATA subdirectory approach required moving the cluster on disk). * fix(docker): apply the postgres-init chown sidecar to dev compose files docker-compose.yml and docker-compose.dev.yml bind-mount ./docker-data/postgres the same way the prod compose does, so they carry the same ownership bug. Add the same postgres-init sidecar to both, gating postgres on it. * fix(docker): move Postgres to a named volume, replacing the postgres-init sidecar The postgres-init chown sidecar didn't address the actual reported bug (#485): docker compose build fails at "load build context" because Postgres locks ./docker-data/postgres down to 0700 once it's run, and BuildKit's context walker needs to stat every entry under the build context root regardless of .dockerignore rules — a permission-denied directory anywhere in that tree aborts the whole context transfer, even when .dockerignore would ultimately exclude it. Verified empirically: fixing .dockerignore alone does not resolve the build failure; removing the directory from the build context (named volume) does. Switch Postgres's volume from a bind mount to the named Docker volume testplanit-postgres-data across all three compose files, so its data directory can never live inside the build context. Also fix the .dockerignore inconsistency across the three ignore files as a secondary hygiene fix, even though it isn't sufficient on its own. Existing deployments need a one-time migration (documented in docker-setup.md) to move their bind-mounted data into the named volume and remove the old directory, since a leftover 0700 directory would still break the build even after upgrading. * feat(docker): bump Postgres from 15 to 18 15 is three majors behind current stable (18.4). No blocking compatibility issues found: the app only uses the pg_trgm extension (stable since early Postgres releases), and the audit trigger installer uses only long-standing PL/pgSQL/SQL constructs. Verified against a real Postgres 18 instance: Prisma client generation, `prisma db push`, all 69 audit triggers, and the full demo seed all complete cleanly, and data survives a container restart. Postgres 18's official image changed its default PGDATA to a version-specific subdirectory and expects the volume mounted at /var/lib/postgresql instead of .../data (docker-library/postgres#1259). Pin PGDATA back to the old flat path so the existing named-volume mount doesn't need to change on every future major bump, and to sidestep a still-open detection bug in the new layout (docker-library/postgres#1400) that reproduced reliably in local testing. Existing deployments need a real pg_dump/pg_restore migration, not a raw file copy — Postgres 18 can't read a Postgres 15 data directory. Documented alongside the named-volume migration note, since both land in the same upgrade.
1 parent 4cc9056 commit a8da511

8 files changed

Lines changed: 131 additions & 14 deletions

File tree

docs/.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ docker-compose.yml
1111
*.log
1212
.DS_Store
1313
.vscode/
14+
**/docker-data/

docs/docs/deployment.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,17 @@ docker compose -f docker-compose.prod.yml $PROFILES up -d
232232
# Backup data (if using Docker services)
233233
docker compose -f docker-compose.prod.yml $PROFILES down
234234
tar -czf testplanit-backup-$(date +%Y%m%d).tar.gz docker-data/
235+
docker run --rm -v testplanit-postgres-data:/data -v "$(pwd):/backup" \
236+
alpine tar -czf /backup/testplanit-postgres-backup-$(date +%Y%m%d).tar.gz -C /data .
235237

236238
# Restore from backup
237239
docker compose -f docker-compose.prod.yml $PROFILES down
238240
sudo rm -rf docker-data/
241+
docker volume rm testplanit-postgres-data
239242
tar -xzf testplanit-backup-YYYYMMDD.tar.gz
243+
docker volume create testplanit-postgres-data
244+
docker run --rm -v testplanit-postgres-data:/data -v "$(pwd):/backup" \
245+
alpine tar -xzf /backup/testplanit-postgres-backup-YYYYMMDD.tar.gz -C /data
240246
docker compose -f docker-compose.prod.yml $PROFILES up -d
241247
```
242248

@@ -273,13 +279,15 @@ docker exec testplanit-workers pm2 logs notification-worker
273279

274280
### Data Persistence
275281

276-
All service data is stored in `./docker-data/`:
282+
Most service data is stored in `./docker-data/`:
277283

278-
- `postgres/` - Database files
279284
- `redis/` - Valkey persistence
280285
- `elasticsearch/` - Search indexes
281286
- `minio/` - File attachments
282287

288+
Postgres data lives in the named Docker volume `testplanit-postgres-data` instead
289+
of a bind mount, so it can never end up inside the build context.
290+
283291
### File Storage Configuration
284292

285293
**Option 1: Docker MinIO** (`with-minio` profile)

docs/docs/docker-setup.md

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ docker compose down --remove-orphans
247247
# Fresh start (removes all data!)
248248
docker compose down
249249
sudo rm -rf docker-data/
250+
docker volume rm testplanit-postgres-data
250251
docker compose up prod workers --build
251252
```
252253
@@ -307,16 +308,63 @@ docker compose -f docker-compose.prod.yml \
307308
308309
### Data Persistence
309310
310-
All service data persists in `./docker-data/`:
311+
Most service data persists in `./docker-data/`:
311312
312313
```text
313314
docker-data/
314-
├── postgres/ # Database files
315315
├── valkey/ # Valkey job queue persistence
316316
├── elasticsearch/ # Search indexes
317317
└── minio/ # File attachments
318318
```
319319
320+
Postgres is the exception: it uses the named Docker volume `testplanit-postgres-data`
321+
instead of a bind mount. Its data directory can't live inside `./docker-data/`
322+
(or anywhere else under the build context) — once Postgres has run, it locks the
323+
directory down to `0700`, which makes `docker compose build` fail with a
324+
permission error on any later build.
325+
326+
Postgres was also bumped from 15 to 18 in the same change, so this isn't a
327+
straight file-copy upgrade — Postgres 18 cannot read a Postgres 15 data
328+
directory. Existing deployments need a `pg_dump` / `pg_restore` migration
329+
instead.
330+
331+
:::warning Upgrading an existing deployment
332+
333+
Deployments created before this change run Postgres 15 with data directly in
334+
`docker-data/postgres/`. Dump the database **before** switching to the
335+
updated compose files — once you do, `postgres` starts on the new image and
336+
can no longer read the old data directory:
337+
338+
```bash
339+
# 1. On the OLD stack (still Postgres 15), dump the database.
340+
docker compose exec -T postgres pg_dump -U user -Fc -d testplanit_prod > testplanit-backup.dump
341+
docker compose down
342+
```
343+
344+
Pull the updated compose files, then bring up a fresh Postgres 18 instance and restore into it:
345+
346+
```bash
347+
# 2. Bring up the new (empty) Postgres 18 named volume and restore.
348+
docker compose up postgres -d
349+
# wait for it to report healthy, then:
350+
docker compose exec -T postgres pg_restore -U user -d testplanit_prod --clean --if-exists < testplanit-backup.dump
351+
352+
# 3. Verify the app reads the migrated data correctly, then bring up the rest.
353+
docker compose up prod workers -d
354+
```
355+
356+
Once you've confirmed the migrated data is intact, the old `docker-data/postgres/`
357+
directory is no longer used and can be removed. It's already locked down to
358+
`0700` by the old Postgres process, so removing it needs a root context — either
359+
`sudo rm -rf docker-data/postgres`, or from a throwaway container that doesn't
360+
need host-level `sudo`:
361+
362+
```bash
363+
docker run --rm -v "$(pwd)/docker-data:/data" alpine rm -rf /data/postgres
364+
```
365+
366+
:::
367+
320368
### Backup & Restore
321369

322370
**Create Backup:**
@@ -325,9 +373,15 @@ docker-data/
325373
# Stop services
326374
docker compose down
327375
328-
# Create timestamped backup
376+
# Back up the bind-mounted service data
329377
tar -czf testplanit-backup-$(date +%Y%m%d).tar.gz docker-data/
330378
379+
# Back up the Postgres named volume separately
380+
docker run --rm \
381+
-v testplanit-postgres-data:/data \
382+
-v "$(pwd):/backup" \
383+
alpine tar -czf /backup/testplanit-postgres-backup-$(date +%Y%m%d).tar.gz -C /data .
384+
331385
# Restart services
332386
docker compose up prod workers -d
333387
```
@@ -338,10 +392,18 @@ docker compose up prod workers -d
338392
# Stop and remove current data
339393
docker compose down
340394
sudo rm -rf docker-data/
395+
docker volume rm testplanit-postgres-data
341396
342-
# Extract backup
397+
# Extract the bind-mounted service data
343398
tar -xzf testplanit-backup-YYYYMMDD.tar.gz
344399
400+
# Restore the Postgres named volume
401+
docker volume create testplanit-postgres-data
402+
docker run --rm \
403+
-v testplanit-postgres-data:/data \
404+
-v "$(pwd):/backup" \
405+
alpine tar -xzf /backup/testplanit-postgres-backup-YYYYMMDD.tar.gz -C /data
406+
345407
# Restart services
346408
docker compose up prod workers -d
347409
```
@@ -507,5 +569,6 @@ docker exec testplanit-workers pm2 list
507569
# Nuclear option - fresh start
508570
docker compose down
509571
sudo rm -rf docker-data/
572+
docker volume rm testplanit-postgres-data
510573
docker compose up prod workers --build
511574
```

testplanit/.dockerignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ README.md
1111
.DS_Store
1212
dist
1313
coverage
14-
docker-data/
14+
**/docker-data/

testplanit/docker-compose.dev.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,24 @@ services:
8282

8383
postgres:
8484
container_name: testplanit-postgres
85-
image: postgres:15-alpine # Use a specific Postgres version
85+
image: postgres:18-alpine # Use a specific Postgres version
8686
environment:
8787
POSTGRES_USER: user # Must match the user in DATABASE_URL
8888
POSTGRES_PASSWORD: password # Must match the password in DATABASE_URL
8989
POSTGRES_DB: testplanit_dev # Creates this DB initially. Prod app uses testplanit_prod
90+
# Postgres 18 defaults PGDATA to a version-specific subdirectory
91+
# (/var/lib/postgresql/18/docker) and expects the volume mounted at
92+
# /var/lib/postgresql instead of .../data. Pin the old flat layout so the
93+
# mount path below doesn't have to change across major-version bumps, and
94+
# to avoid a still-open docker-library/postgres detection bug around the
95+
# new layout (github.com/docker-library/postgres/issues/1400).
96+
PGDATA: /var/lib/postgresql/data
97+
# Named volume, not a bind mount: Postgres's data directory must never live
98+
# inside the build context (context: .. at the repo root). Once Postgres has
99+
# run, it chmods the directory 0700, which makes BuildKit's context walker
100+
# fail with "permission denied" on any subsequent `docker compose build`.
90101
volumes:
91-
- ./docker-data/postgres:/var/lib/postgresql/data
102+
- postgres-data:/var/lib/postgresql/data
92103
ports:
93104
- "${DOCKER_POSTGRES_PORT:-5432}:5432" # Expose Postgres on configurable host port
94105
networks:
@@ -117,3 +128,7 @@ services:
117128
networks:
118129
testplanit-net:
119130
driver: bridge
131+
132+
volumes:
133+
postgres-data:
134+
name: testplanit-postgres-data

testplanit/docker-compose.prod.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,24 @@ services:
8080

8181
postgres:
8282
container_name: testplanit-postgres
83-
image: postgres:15-alpine
83+
image: postgres:18-alpine
8484
environment:
8585
POSTGRES_USER: user
8686
POSTGRES_PASSWORD: password
8787
POSTGRES_DB: testplanit_prod
88+
# Postgres 18 defaults PGDATA to a version-specific subdirectory
89+
# (/var/lib/postgresql/18/docker) and expects the volume mounted at
90+
# /var/lib/postgresql instead of .../data. Pin the old flat layout so the
91+
# mount path below doesn't have to change across major-version bumps, and
92+
# to avoid a still-open docker-library/postgres detection bug around the
93+
# new layout (github.com/docker-library/postgres/issues/1400).
94+
PGDATA: /var/lib/postgresql/data
95+
# Named volume, not a bind mount: Postgres's data directory must never live
96+
# inside the build context (context: .. at the repo root). Once Postgres has
97+
# run, it chmods the directory 0700, which makes BuildKit's context walker
98+
# fail with "permission denied" on any subsequent `docker compose build`.
8899
volumes:
89-
- ./docker-data/postgres:/var/lib/postgresql/data
100+
- postgres-data:/var/lib/postgresql/data
90101
ports:
91102
- "${DOCKER_POSTGRES_PORT:-5432}:5432"
92103
networks:
@@ -219,3 +230,7 @@ services:
219230
networks:
220231
testplanit-net:
221232
driver: bridge
233+
234+
volumes:
235+
postgres-data:
236+
name: testplanit-postgres-data

testplanit/docker-compose.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,24 @@ services:
199199

200200
postgres:
201201
container_name: testplanit-postgres
202-
image: postgres:15-alpine # Use a specific Postgres version
202+
image: postgres:18-alpine # Use a specific Postgres version
203203
environment:
204204
POSTGRES_USER: user # Must match the user in DATABASE_URL
205205
POSTGRES_PASSWORD: password # Must match the password in DATABASE_URL
206206
POSTGRES_DB: testplanit_dev # Creates this DB initially. Prod app uses testplanit_prod
207+
# Postgres 18 defaults PGDATA to a version-specific subdirectory
208+
# (/var/lib/postgresql/18/docker) and expects the volume mounted at
209+
# /var/lib/postgresql instead of .../data. Pin the old flat layout so the
210+
# mount path below doesn't have to change across major-version bumps, and
211+
# to avoid a still-open docker-library/postgres detection bug around the
212+
# new layout (github.com/docker-library/postgres/issues/1400).
213+
PGDATA: /var/lib/postgresql/data
214+
# Named volume, not a bind mount: Postgres's data directory must never live
215+
# inside the build context (context: .. at the repo root). Once Postgres has
216+
# run, it chmods the directory 0700, which makes BuildKit's context walker
217+
# fail with "permission denied" on any subsequent `docker compose build`.
207218
volumes:
208-
- ./docker-data/postgres:/var/lib/postgresql/data
219+
- postgres-data:/var/lib/postgresql/data
209220
ports:
210221
- "${DOCKER_POSTGRES_PORT:-5432}:5432" # Expose Postgres on configurable host port
211222
networks:
@@ -325,3 +336,7 @@ services:
325336
networks:
326337
testplanit-net:
327338
driver: bridge
339+
340+
volumes:
341+
postgres-data:
342+
name: testplanit-postgres-data

testplanit/e2e/a11y/ci-workflow.draft.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232

3333
services:
3434
postgres:
35-
image: postgres:15-alpine
35+
image: postgres:18-alpine
3636
env:
3737
POSTGRES_USER: user
3838
POSTGRES_PASSWORD: password

0 commit comments

Comments
 (0)