Plug.Static is unable to serve files in production

Hi everyone,

I have been banging my head against a wall for the last couple of days because of a really weird problem I’m having in production.

I have a very simple Phoenix app that lets an admin user upload an image when creating a “note”. I’m using arc and arc-ecto to handle the file uploads. Everything works great in development. I’m able to upload the images without issues, and I can see them on the “notes” page.

However, when the app is running in production, I am able to upload the images without errors, but Phoenix is giving me a 404 when trying to fetch the images.

I already added the Plug.Static declaration to may endpoint.ex file, which is why I am able to see the images in dev, but no matter what I try in production, I always get a 404.

I have this in my endpoint.ex file. I already tried replacing Path.expand("./uploads") with the full path of where the files are when in production, and no luck.

plug Plug.Static,
    at: "/uploads",
    from: Path.expand("./uploads/"),
    gzip: false

I know Elixir has access to this folder in production, because after uploading the images, they are there.

I’m using Distillery to create the release for production, and I’m deploying the app as a Docker image using docker-compose. I already made sure that the destination of the uploaded files is mounted as a volume on the Docker image, so that they don’t get destroyed after each deploy.

What could possibly be happening here?

I’m really close to just giving up trying to get Plug.Static to work in prod, and just Base64 encode the images and inlining them in the HTML file.

Any help, or guidance you can give me is greatly appreciated!

WHat does Path.expand("./uploads/") return? Is it as expected and really the folder you have configured as target for copying the uploaded files to? relative pathes can be mean at times, especially if one isn’t careful or conscious about the actual working directory.

Sadly you haven’t told us how you are starting your application… That could give some hints about the actual working directory.

1 Like

See this excerpt from the docs

The preferred form is to use :from with an atom or tuple, since it will make your application independent from the starting directory. For example, if you pass:

    plug Plug.Static, from: "priv/app/path"

Plug.Static will be unable to serve assets if you build releases or if you change the current directory. Instead do:

    plug Plug.Static, from: {:app_name, "priv/app/path"}

have you tried it with tuple?

2 Likes

@wojtekmach I should have mentioned I tried that as well without any luck.

@NobbZ The path I want to access is /app/uploads. Docker mounts the app in /app so if I run a remote console to the running app in production and do Path.expand("./uploads") I get the correct path: /app/uploads. Plus, Arc is able to write to the correct path without any further modifications.

I think posting my Docker file might help:

# Build Stage
FROM elixir:1.9 as build

ENV DEBIAN_FRONTEND=noninteractive
ENV HOME=/opt/app/ TERM=xterm

RUN \
  curl -sL https://deb.nodesource.com/setup_10.x | bash - && \
  curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
  echo "deb http://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
  apt-get update && apt-get -y install nodejs yarn

## Install Hex+Rebar
RUN mix local.hex --force && \
  mix local.rebar --force

WORKDIR /opt/app

ENV MIX_ENV=prod

## Cache elixir deps
RUN mkdir config
COPY config/* config/
COPY mix.exs mix.lock ./
RUN mix do deps.get, deps.compile

## Cache node deps
COPY assets/*.json ./assets/
COPY assets/yarn.lock ./assets/
RUN cd ./assets && yarn install

## Compile assests & create digest
COPY . .
RUN npm run deploy --prefix ./assets
RUN mix phx.digest

## Compile Elixir release
RUN MIX_ENV=prod mix distillery.release --env=prod

# Release Stage
FROM elixir:1.9-slim

EXPOSE 4000
ENV MIX_ENV=prod REPLACE_OS_VARS=true SHELL=/bin/sh

WORKDIR /app
COPY --from=build /opt/app/_build/prod/rel/c37 ./

ENTRYPOINT ["bin/c37"]

The app is started when docker-compose creates the web service with the foreground option

Really should set it to /app/uploads then, don’t use relative paths without the tuple format and even then only within an application itself, not outside of it. The CWD can change in unexpected ways in a massively concurrent system.

I am curious if Plug.Static requires things it is serving to be local though, hmm…

Thanks for the suggestion, but I already tried “hardcoding” the full path, and it did not work either.

Is it possible to define environment specific plugs, or would having 2 plugs be good enough? The paths are different on prod and dev, so if I hardcode them to “/app/uploads” dev breaks.

As plug is a macro, I’m not even sure if it resolve the path during compile time or runtime. Wrap an IO.insoect and watch application build as well as start carefully for output. The :label option is of great help for such things.

At compile-time for Plug.Static.

And I just tested it here with an absolute directory and it worked, so there is something else going on that’s possibly not related to the path for Plug.Static?

Yeah, I thought about that myself, but I really do not want to start a marathon of a debugging session when simply a Base64 encode of the image will solve my issues. Once this application gets more usage, uploaded assets will be stored on S3 anyways, but I just can’t fight this nagging feeling. What I’m trying to do should work.I just can’t figure out why.

1 Like

Any chance at making an SSCCE that we can just git clone and spool up or so?

Are you working with a Docker image as well?

Let me see if I can get one ready within 15 minutes

No but if that changed things that leans to a bug in docker as this is just simple filesystem work?

:thinking: that is an easy thing to test. It never occurred to me to test the release compiled app without docker. If that one works, then it must be a bug with docker, or something.

1 Like

Or some interaction with the filesystem being used perhaps. What’s the host OS and FS too?

I gave up on trying to get it to work. I decided to just Base64 encode the image and send it inline within the HTML data. This is working perfectly in production now, which means it is probably not an issue with the OS or file system.

Once the app gets proper usage, I’ll be redoing most of the infrastructure, so images will be stored on S3, which will make this problem irrelevant anyhow.

Anyways if somebody comes across this problem in the future, let me know. I can revisit it then, and we can work on it together.

1 Like

In case someone else is looking for a solution. I had a similar issue.

In my endpoint.ex I had …

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

  plug Plug.Static,
    at: "/",
    from: {:fast_inventory_system, "priv/stock_images"},
    gzip: true

But the code which handled the uploads put it in the wrong directory. It only worked locally, but not when we deployed with releases.

File.write!("priv/stock_images", "foo.jpg")

It needs to consider the app_dir:

File.write!(Application.app_dir(:fast_inventory_system, "priv/stock_images/foo.jpg")

Hope this helps

Instead of changing where to place the file i’ve adjusted the source directory. Useful for edeliver/distillery setup.

plug Plug.Static, at: "/uploads", from: {:your_app, "../../uploads"}

I spent the entire afternoon and half a morning trying to fix this issue as well.

Turns out that in production (with Gigalixir) Path.expand goes to app/uploads which @supernova32 is aware of, but the upload goes to the same place that it did (I’m using Waffle/the updated Arc library)

The simple solution is to simply hardcode the directory from which we are serving the assets from.

i.e.

  plug(Plug.Static,
    at: "/uploads",
    from: "./uploads",
    gzip: false
  )

instead of

from: Path.expand("./uploads/"),

I haven’t tested it again in a dev environment but hopefully it should help someone else save some time, because this is a very simple fix that shouldn’t have taken me, or anyone else, more than 15 minutes :frowning: .

2 Likes

Hi all,

I have issue with retrieving/accessing the images, my images are saved under priv/static/waffle/uploads/ and this is my plug:

  plug Plug.Static,
    at: "/uploads",
    from: {:pix_tools, "./priv/static/waffle/uploads"},
    gzip: false

and I am getting Phoenix.Router.NoRouteError

need help…