Persistent Storage
Every deploy on Potions unpacks a fresh release into a clean slot directory (blue/green) and then switches traffic. Because of this, anything written to the release directory is ephemeral (including priv/static/uploads/) and doesn't survive between deploys.
To solve this, Potions auto-provisions a per-app persistent storage directory.
The Basics
Every app gets a directory at /opt/potions/<app_name>/storage on the server. It's exposed two ways:
-
The
STORAGE_DIRenvironment variable (auto-generated and visible on your Environment tab) -
A
./storagesymlink inside each release slot, so relative paths from your app's working directory also work
Files written here:
- ✅ Survive deploys - the directory lives next to (not inside) the release slots
- ✅ Survive rollbacks - same physical directory, regardless of which slot is active
-
❌ Are deleted when the app is deleted -
rm -rf /opt/potions/<app>removes everything
Writing Files
Read the path from STORAGE_DIR and use it like any other directory. For local development, where STORAGE_DIR isn't set, you can add a fallback in config/runtime.exs:
# config/runtime.exs
storage_dir = System.get_env("STORAGE_DIR") || Path.expand("../storage", __DIR__)
File.mkdir_p!(storage_dir)
config :my_app, :storage_dir, storage_dir
Don't forget to add /storage to your .gitignore. You can use the directory however fits your app - subdirectories like images/, videos/, or per-user namespaces all work. Subdirectories you create at runtime inherit the same deploy:deploy ownership as the storage root.
Serving Files Publicly
Because files in STORAGE_DIR are outside priv/static/, Phoenix's default static handler doesn't see them. To serve them you'll need to add a separate Plug.Static entry to your endpoint:
# lib/my_app_web/endpoint.ex
plug :serve_uploads
defp serve_uploads(conn, _opts) do
Plug.Static.call(
conn,
Plug.Static.init(
at: "/uploads",
from: Application.fetch_env!(:my_app, :storage_dir),
gzip: false
)
)
end
Why the wrapper?
plug Plug.Static, ...reads its options at compile time, butSTORAGE_DIRis set at runtime. The function plug defers the lookup.
Things to Know
- No automatic cleanup. Files here aren't automatically cleaned up, so disk usage grows over time. If you anticipate storing many files here, consider cloud storage.
-
The path is reserved. You can't manually add or override a
STORAGE_DIRenv var. This prevents accidental misconfiguration.