config: DATA_DIR chooses the upload directory - #32
Conversation
The upload directory (avatars, branding assets) was the literal relative "data" in server.BuildHandler, which every deployment has been writing to since the beginning and which a read-only container image running as an unprivileged user cannot create: the first avatar upload fails with EACCES and nothing at boot says why. DATA_DIR sets it, defaulting to the same relative "data", so nothing moves for anyone who never sets it. BuildHandler repeats the fallback because tests build a Config literal that skips Load, and an empty directory would put uploads beside the binary.
There was a problem hiding this comment.
Important
DATA_DIR is wired correctly in config, but the official container and deploy docs never set it, so the EACCES fix does not land by default for the documented Docker/Railway path unless the process cwd happens to be /.
Reviewed changes of pr/data-dir vs main (1 commit): adds a DATA_DIR config knob for the avatar/branding upload root, defaulting to the existing relative data path.
- Config field —
Config.DataDirloaded viagetEnv("DATA_DIR", "data"), with a comment explaining the container/volume use case. - Handler wiring —
BuildHandlerpassescfg.DataDirintoSetDataDir, with an empty→"data"fallback for testConfigliterals that skipLoad. - Test —
TestLoad_dataDirpins default and override.
⚠️ Official image and docs still leave uploads cwd-relative
The PR’s motivation is read-only containers where relative data is not writable. The code change is sound and backward-compatible, but the deploy surface that already pins the DB to /data never opts into DATA_DIR, so operators following DEPLOY.md / the image defaults still depend on process cwd.
Today the Alpine image has no WORKDIR, so cwd is / and relative data already lands under the /data volume. That is accidental coupling: add a WORKDIR, run the binary under systemd with another working directory, or drop privileges without write on cwd, and uploads fail again while the DB path stays correct. docs/ARCHITECTURE.md already claims branding is “stored on the /data volume,” which is only true under that cwd accident or an explicit DATA_DIR.
Technical details
# Finish the DATA_DIR rollout for containers
## Affected sites
- `Dockerfile` — sets `DATABASE_URL=sqlite:///data/calnode.db` but not `DATA_DIR`; pair them (`ENV DATA_DIR=/data` next to the existing `DATABASE_URL` default).
- `DEPLOY.md` §1 env table + Docker/Railway examples — document `DATA_DIR` (default `data`; container recommendation `/data` alongside the volume).
- `.env.example` — add a short commented `DATA_DIR` note.
- `docs/ARCHITECTURE.md` §3 key vars — list `DATA_DIR` next to `DATABASE_URL`.
## Required outcome
- Official image writes avatars/branding under the same persistent volume as SQLite without relying on cwd.
- Operators reading deploy docs know the variable exists and what to set for a `/data` mount.
- Default remains relative `data` for non-container / unset-env deploys (no silent path move).
## Suggested approach (optional)
- `Dockerfile`: `ENV PORT=3000 DATABASE_URL=sqlite:///data/calnode.db DATA_DIR=/data`
- One-line table row in `DEPLOY.md`: `DATA_DIR` | no | `data` | Upload root (avatars, branding). Use `/data` when the DB volume is mounted there.Grok | 𝕏

The upload directory (avatars, branding assets) was the literal relative
datainserver.BuildHandler, which every deployment has been writing to and which a read-only container image running as an unprivileged user cannot create: the first avatar upload fails withEACCES, and nothing at boot says why.DATA_DIRsets it, defaulting to the same relativedata, so nothing moves for anyone who never sets it.BuildHandlerrepeats the fallback because tests build aConfigliteral that skipsLoad.Standalone on
main; one commit, one test.🤖 Generated with Claude Code