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_DIR environment variable (auto-generated and visible on your Environment tab)
  • A ./storage symlink 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, but STORAGE_DIR is 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_DIR env var. This prevents accidental misconfiguration.