Create the uploads directory in the runtime image (PMS-757) #511

Merged
longjacksonle merged 1 commit from fix/PMS-757-uploads-dir-in-image into main 2026-08-10 20:07:26 +02:00

PMS-757. Half of a two-repo fix; the volume itself is NiceGuyIT/docker #332, and this must merge and be built first.

The problem

Nothing on staging or production mounts a volume for uploads. template/mokosh-server/compose.yml gives the server service an image, environment, network, healthcheck and labels, and no volumes: key at all; the only declared volume belongs to postgres. ATTACHMENT_DIR is unset on both hosts, so it falls back to ./attachments against WORKDIR /app, which is the container's writable layer, and pull_policy: always recreates that layer on every deploy.

So every ticket-note attachment (PMS-483) and every tenant logo (MAPPS-429) is destroyed by the next deploy while its database row survives. For the logo that is worse than it sounds: branding.logo_url still points at the serving route, so the request-form email renders a broken image rather than no image, which is the version a client sees.

Not a regression in either ticket. It has been true since PMS-483 shipped the first upload path, and MAPPS-429 inherited it by reusing that storage pattern without anyone checking the pattern was deployed correctly. I reused it, so that one is mine.

Why this half goes first

Docker seeds an empty named volume from whatever the image holds at the mount point, ownership included. With no directory in the image, a fresh volume is root-owned, and this container runs as uid 1001. Attaching the volume without this change would break every upload that currently works, on the exact deploy meant to fix them.

/app/attachments rather than a new path, so the image, the code default and the mount all name the same place and a bare docker run behaves like a deployment.

Testing

just check-docker passes. Verified against a fully built runtime image rather than the builder stage, since the directory only exists in the latter:

drwxr-xr-x 1 appuser appuser 4096 /app/attachments
uid=1001(appuser) gid=1001(appuser)

Nothing else changes: no code, no migration, no behaviour until the volume lands.

PMS-757. Half of a two-repo fix; the volume itself is NiceGuyIT/docker #332, and **this must merge and be built first**. ## The problem Nothing on staging or production mounts a volume for uploads. `template/mokosh-server/compose.yml` gives the `server` service an image, environment, network, healthcheck and labels, and no `volumes:` key at all; the only declared volume belongs to postgres. `ATTACHMENT_DIR` is unset on both hosts, so it falls back to `./attachments` against `WORKDIR /app`, which is the container's writable layer, and `pull_policy: always` recreates that layer on every deploy. So every ticket-note attachment (PMS-483) and every tenant logo (MAPPS-429) is destroyed by the next deploy while its database row survives. For the logo that is worse than it sounds: `branding.logo_url` still points at the serving route, so the request-form email renders a broken image rather than no image, which is the version a client sees. Not a regression in either ticket. It has been true since PMS-483 shipped the first upload path, and MAPPS-429 inherited it by reusing that storage pattern without anyone checking the pattern was deployed correctly. I reused it, so that one is mine. ## Why this half goes first Docker seeds an empty named volume from whatever the image holds at the mount point, ownership included. With no directory in the image, a fresh volume is root-owned, and this container runs as uid 1001. Attaching the volume without this change would break every upload that currently works, on the exact deploy meant to fix them. `/app/attachments` rather than a new path, so the image, the code default and the mount all name the same place and a bare `docker run` behaves like a deployment. ## Testing `just check-docker` passes. Verified against a fully built runtime image rather than the builder stage, since the directory only exists in the latter: ``` drwxr-xr-x 1 appuser appuser 4096 /app/attachments uid=1001(appuser) gid=1001(appuser) ``` Nothing else changes: no code, no migration, no behaviour until the volume lands.
fix(oci): create the uploads directory in the runtime image
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 2m24s
E2E / Playwright against staging (pull_request) Successful in 2m57s
Create release / Gate (release-branch merges only) (pull_request) Successful in 1s
Create release / Create release from merged PR (pull_request) Has been skipped
Integration / integration tests (pull_request) Successful in 13m40s
6ebc4e4f2b
PMS-757. Nothing on staging or production mounts a volume for uploads, so every ticket attachment and every tenant logo lives in the container's writable layer and dies with the next deploy. The database rows survive and keep pointing at files that are gone.

This is the half that has to land first. Docker seeds an empty named volume from whatever the image holds at the mount point, ownership included. With no directory in the image the fresh volume is root-owned, and this container runs as uid 1001, so attaching a volume without this change would break every upload that currently works, on the very deploy meant to fix them.

`/app/attachments` rather than a new path: `ATTACHMENT_DIR` defaults to `./attachments` against `WORKDIR /app`, so the image, the default and the mount all name the same place, and a bare `docker run` behaves like a deployment.

Verified against a built runtime image: the directory exists and is owned by appuser.

The matching volume and `ATTACHMENT_DIR` land in NiceGuyIT/docker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E48b8YHWYwmaKjFBy5esXp
longjacksonle deleted branch fix/PMS-757-uploads-dir-in-image 2026-08-10 20:07:26 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!511
No description provided.