Serving static files outside of the app directory

Hi all,

After following the LiveView guide for live file uploads, I am able to have users upload files. This works great in dev, but I’m questioning the best way to serve the uploads in production.

The uploads directory in the docs is priv/static/uploads. To serve the files, I added the uploads dir to the static paths for the Plug.Static plug, which works great for dev.

However, when deploying on fly.io, I want to store the files on a volume. To do so, I created the volume and mounted it in /mnt/myvolume. Now, I created some application config to read the location where it should put the uploaded files - priv/static/uploads for :dev and /mnt/myvolume/uploads for :prod.

This is where I get stuck - it would be best if this is configured in runtime.exs, so I can change volumes, path, etc with just a environment variable change, and not a whole build via the fly deploy pipeline. But the plug is configured at compile time in endpoint.ex:

  plug Plug.Static,
    at: "/",
    from: :my_app,
    gzip: false,
    only: ~w(assets fonts images favicon.ico robots.txt)

  plug Plug.Static,
    at: "/uploads",
    from: Application.get_env(:my_app, :upload_path)

And config for the above:

dev.exs
config :my_app, :upload_path, "priv/static/uploads"

prod.exs
config :my_app, :upload_path, "/mnt/myvolume/uploads"

The approach above works in serving the files, but I don’t have runtime config control because the options are passed to the plug at runtime.

I tried some implementations from other threads in the forum of configuring plugs at runtime, such as this one: Session cookie domain option - #4 by nietaki

I couldn’t get it to work correctly, so asking for guidance here. Some alternatives I think I could see:

  • mount the volume at the location of the priv/static/uploads directory, and just let it do it’s thing. I don’t like this because the priv directory could change locations. In the release, it is at _build/prod/rel/my_app/lib/my_app-0.1.0/priv, which could change when I bump versions in mix.exs.
  • I could also symlink that above directory to the mount location? Again, I don’t like this because the path could change, and this would have to be in the build context of the docker image. And I don’t know if linux would be happy to symlink to a path that doesn’t exist before the volume is mounted?
  • I can bite the bullet and get started with S3 uploads. I will do this eventually when I launch my app, but I’m really hoping to hold off with AWS for a little bit longer…
  • ???

Thanks!

1 Like

Update: I figured it out. The runtime config plug below worked for dev but not prod (which is why I said I was couldn’t get it to work correctly). Turns out the problem was that I recently changed the config name from config :my_app, MyApp.SomeModule, upload_path: "..." to config :my_app, :upload_path, "...", but didn’t catch the change in runtime.exs. That would do it.

I definitely was staring at this for too long, I need to get out and go for a walk :sweat_smile:

The plug which worked for me:

defmodule MyAppWeb.Plugs.Uploads do
  def init(opts), do: Plug.Static.init(opts)

  def call(conn, opts) do
    runtime_opts = Map.put(opts, :from, Application.get_env(:my_app, :upload_path))
    Plug.Static.call(conn, runtime_opts)
  end
end
3 Likes