← Blog

PrintStash v0.12.1: container startup reliability

Why some API containers crash-looped right after their database migrations finished, the two environment variables that fix it, and who needs to care.

release

v0.12.1 is a one-fix reliability patch for the API image. It changes nothing about features, the database schema, or configuration. If your containers start cleanly today, upgrading is still just pulling the new image, and you can stop reading here. If you run PrintStash with Watchtower-style automatic updates, or you have ever overridden the container command, this patch is for you.

What went wrong

On August 20, reports came in that printstash-api:latest was crash-looping: the container started, ran every database migration to completion, and died immediately after, over and over (issue #77). One reporter counted more than 250 restart attempts across several hours. The frontend stayed up the whole time, which made it look worse than it was: behind it sat an API that never became healthy.

The error at the bottom of every failed start:

error: Failed to initialize cache at `/tmp/.uv_cache`
Caused by: failed to open file `/tmp/.uv_cache/sdists-v9/.git`: Permission denied (os error 13)

That path belongs to uv, the package manager the image uses at build time. The image builds its Python environment as root and then runs the application as an unprivileged user named printstash. In v0.12.0, the build stopped handing the uv cache directory to that user, and the full image’s Chromium install step recreated it as root-owned afterward. The cache path also leaked into runtime through an environment variable meant for the build stage.

None of that mattered for the shipped startup path. The entrypoint runs migrations and then executes uvicorn straight from the virtualenv, so a stock Compose deployment never invokes uv at runtime and never touched the cache. The failure needed a second ingredient: a deployment that overrides the container command or otherwise starts the app through uv run, which plenty of older Compose files and operator customizations still do. When such a deployment pulled :latest, uv tried to initialize the root-owned cache as the unprivileged user, failed, and took the container down with it. Migrations finishing cleanly every cycle made the loop look baffling; they were never the problem.

What changed

Two environment variables, set late in the Dockerfile so they apply at runtime while leaving the build stage alone:

  • UV_CACHE_DIR=/tmp/printstash-uv-cache gives any runtime uv invocation a fresh cache directory the printstash user can write.
  • UV_NO_SYNC=1 stops uv from re-syncing the environment on each invocation. Dependencies are locked and baked into the image at build time; re-checking them at runtime is wasted work even when it succeeds.

A regression test now asserts both values, their position after the unprivileged user is created, and that the shipped entrypoint and CMD still execute the virtualenv directly.

If you maintain a custom command override, it keeps working, and it now does so without depending on the permissions of a directory nothing in the shipped image writes to at runtime. If you also want to drop the legacy path entirely, the direct form is what the image itself ships:

command: ["/app/.venv/bin/uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Upgrading

No database migrations and no configuration changes. Pull and restart:

Terminal window
docker compose pull
docker compose up -d --wait

Watchtower and similar auto-updaters need nothing extra; the fixed images are already what :latest resolves to. As always, take a backup first if you are many versions behind, and the upgrade guide has the full routine. The complete change list lives in the v0.12.1 release notes.