Why your app gets "permission denied" writing files at runtime
Your app builds. It starts. Then it dies the first time it tries to write a file, with something like:
PermissionError: [Errno 13] Permission denied: '/app/.cache/...'
The confusing part: it worked on your laptop, and the build had no errors. If the app is an AI agent, the line that fails is usually the one that downloads a model on first use. We hit this ourselves on one of our own apps, where an embedding library tried to download its model into /app/.cache and got rejected every time. The step right before it, talking to the database, worked fine, so the error looked unrelated to permissions. It wasn’t.
Here’s what’s going on and how to fix it for good.
What’s actually happening
For isolation, Dockhold runs your app as a fixed non-root user. That user belongs to the root group (group id 0), and it is not necessarily the user your Dockerfile created.
A very common Dockerfile does this:
RUN useradd --uid 10001 appuser \
&& chown -R appuser:appuser /app
USER appuser
That makes /app owned by appuser (uid 10001), writable only by that exact user. On your machine the container runs as appuser, so writes work. On Dockhold the process runs as our fixed user instead, which is neither the owner nor in a group that can write. Every write under /app fails with permission denied.
The key idea: you can’t rely on the user id your Dockerfile set up. Your app needs to be able to write no matter which user it runs as.
Who hits this
Only apps that write files at runtime. For example:
- AI agents that download model weights or datasets on first use (embeddings, transformers, and similar libraries cache models to disk).
- Anything that builds a cache, writes a SQLite file, or renders templates or images to disk.
If your app only reads the files baked into it, you’ll never see this. It’s specifically the runtime write that gets rejected.
Four ways to fix it
In order of preference.
1. Bake fixed assets into the image at build time
If an asset is known ahead of time (a model, a font, a dataset), download or generate it during the build instead of at runtime. Nothing writes at boot, your app starts faster, and there’s no dependency on an external download succeeding when a request comes in.
# Download the model during the build, not on first request
RUN python -c "from fastembed import TextEmbedding; TextEmbedding('BAAI/bge-small-en-v1.5')"
This is the single biggest win for AI apps. A cold start that pulls a few hundred megabytes of weights is slow and fragile. Bake it once.
2. Make runtime-writable folders group-writable
For paths your app genuinely must write to at runtime, hand ownership to the root group and give the group write access. This works no matter which user the app runs as, because the app always runs in the root group:
RUN useradd --create-home --uid 10001 appuser \
&& chown -R appuser:0 /app \
&& chmod -R g=u /app
USER appuser
chown appuser:0 puts the files in group 0, and chmod -R g=u gives the group the same permissions as the owner. After this, writes under /app succeed whichever user runs the app. This is the robust general fix, and it’s worth doing even alongside option 1.
3. Write throwaway data to /tmp
/tmp is always writable. It’s perfect for scratch files you don’t need to keep, and wrong for anything you do, because the filesystem is ephemeral. It resets on every restart and deploy.
4. Keep anything that must survive in a database
The filesystem is not durable storage on Dockhold. Files written to disk are lost when your app restarts. For data that must persist, use the managed database rather than local disk. See databases and storage for how that works.
If you’re deploying an AI agent
This is the one to plan for. Most embedding and model libraries download weights to a cache directory the first time they run. Two things make that smooth:
- Bake the model in at build time (option 1), so first request is fast and offline-safe.
- Point the cache at a writable path and make that path group-writable (option 2). Set the library’s cache directory explicitly (for example
HF_HOME) so you know exactly which folder needs to be writable.
Do both and the “works on my laptop, permission denied in production” gap closes.
The one-line version
Don’t assume your app runs as the user your Dockerfile created. Bake fixed assets in at build time, make any runtime-writable folder group-writable (chown :0 + chmod g=u), and keep durable data in the database, not on disk. New to deploying here? Start with the quickstart.