Phoenix: Add example Dockerfile for releases created from an Umbrella application to the documentation

Created on 15 Oct 2020  路  9Comments  路  Source: phoenixframework/phoenix

(Phoenix version 1.5.6; Elixir version 1.10.0, OTP version 23.0)

Current situation

Phoenix can work in an umbrella application, and building a docker container from a freestanding Phoenix application is also a breeze by using the example Dockerfile as starting point.

However, this dockerfile cannot easily be used/adapted to an umbrella application.

Here are some problems I encountered. I am very much a Docker novice, so there might be better ways to resolve these problems than what I did so far:

  • mix do compile, release needs to be altered to mix do compile, release name_of_release. Easy fix.
  • Many of the paths (like COPY lib lib) need to be altered to ensure that the phoenix-specific commands are executed within the Phoenix sub-application rather than in the root of the umbrella. Not difficult to fix, but it takes some work because there are quite many paths that need to be altered. Also, there are many paths that you need to specify multiple times (once per sub-application in the umbrella). Maybe there is a more general way to do this?
  • npm ci fails for a difficult to debug reason. Turns out it cannot see through the symlinks that point umbrella_app/apps/your_phoenix_subapp/assets/node_modules/phoenix/ to umbrella_app/deps/phoenix/ (and similar for phoenix_html, phoenix_live_view, phoenix_live_dashboard). Currently I fix this by copying deps/phoenix/ to the apps/your_phoenix_subapp/assets/node_modules/phoenix/ manually.
  • mix phx.digest fails, because it cannot find the mix.exs in the sub-application(s). It requires copying the mix.exs of the sub-applications as well.

It is possible to make a "working build" by just doing COPY apps apps at the start. This will however result in very slow builds which cannot be cached because we end up copying a lot of files that are not necessary and non-deterministic (build artefacts etc). So I am trying to figure out how to do it 'correctly'.


Desired situation

I think it would be very useful if we could add an example Dockerfile for phoenix when used in an umbrella application.

Most helpful comment

We are using this to build our API web server which is a phoenix application (editted to remove specifics)

### Compiler
FROM compiler AS builder
ARG MIX_ENV=dev

WORKDIR /app/monolith

COPY mix.exs .
COPY mix.lock . 

COPY apps/app1/mix.exs apps/app1/mix.exs 
COPY apps/app2/mix.exs apps/app2/mix.exs 
COPY apps/app3/mix.exs apps/app3/mix.exs 
COPY apps/server/mix.exs apps/server/mix.exs 

# Add any compile time configs
COPY config/deps_compile_time.exs config/config.exs

ENV MIX_ENV=${MIX_ENV}

RUN HEX_HTTP_TIMEOUT=10000 mix do deps.get, deps.compile

COPY config config
COPY apps apps 

RUN mix release server

### Runner
FROM base AS runner

ARG MIX_ENV=dev

ENV MIX_ENV=${MIX_ENV}

COPY --from=builder /app/devops/scripts/launch.sh /app/launch.sh 

COPY --from=builder /app/monolith/_build/${MIX_ENV}/rel/server/ /app

CMD [ "/app/bin/server", "start" ]

All 9 comments

Yes, a pull request is welcome. I remember a discussion @QuinnWilton on Twitter where I believe they had already written one and addressed some pitfalls. @QuinnWilton, am I remembering it correctly? If so, would you be able to open source it? Thanks.

We are using this to build our API web server which is a phoenix application (editted to remove specifics)

### Compiler
FROM compiler AS builder
ARG MIX_ENV=dev

WORKDIR /app/monolith

COPY mix.exs .
COPY mix.lock . 

COPY apps/app1/mix.exs apps/app1/mix.exs 
COPY apps/app2/mix.exs apps/app2/mix.exs 
COPY apps/app3/mix.exs apps/app3/mix.exs 
COPY apps/server/mix.exs apps/server/mix.exs 

# Add any compile time configs
COPY config/deps_compile_time.exs config/config.exs

ENV MIX_ENV=${MIX_ENV}

RUN HEX_HTTP_TIMEOUT=10000 mix do deps.get, deps.compile

COPY config config
COPY apps apps 

RUN mix release server

### Runner
FROM base AS runner

ARG MIX_ENV=dev

ENV MIX_ENV=${MIX_ENV}

COPY --from=builder /app/devops/scripts/launch.sh /app/launch.sh 

COPY --from=builder /app/monolith/_build/${MIX_ENV}/rel/server/ /app

CMD [ "/app/bin/server", "start" ]

We are using this to build our API web server which is a phoenix application (editted to remove specifics)

### Compiler
FROM compiler AS builder
ARG MIX_ENV=dev

WORKDIR /app/monolith

COPY mix.exs .
COPY mix.lock . 

COPY apps/app1/mix.exs apps/app1/mix.exs 
COPY apps/app2/mix.exs apps/app2/mix.exs 
COPY apps/app3/mix.exs apps/app3/mix.exs 
COPY apps/server/mix.exs apps/server/mix.exs 

# Add any compile time configs
COPY config/deps_compile_time.exs config/config.exs

ENV MIX_ENV=${MIX_ENV}

RUN HEX_HTTP_TIMEOUT=10000 mix do deps.get, deps.compile

COPY config config
COPY apps apps 

RUN mix release server

### Runner
FROM base AS runner

ARG MIX_ENV=dev

ENV MIX_ENV=${MIX_ENV}

COPY --from=builder /app/devops/scripts/launch.sh /app/launch.sh 

COPY --from=builder /app/monolith/_build/${MIX_ENV}/rel/server/ /app

CMD [ "/app/bin/server", "start" ]

I am missing something or is this not missing compiling of Phoenix assets?
Like

RUN npm run --prefix ./assets deploy
RUN mix phx.digest

I still use a design extremely similar today, 2 years later. It needs minimal adjustments for native releases over Distillery, and accounts for compiling Phoenix frontend assets without adding a Node runtime directly to your build image. Elixir 1.11's runtime.exs should remain compatible with this approach as well.

https://github.com/shanesveller/kube-native-phoenix/blob/master/Dockerfile

I am missing something or is this not missing compiling of Phoenix assets?
Like

RUN npm run --prefix ./assets deploy
RUN mix phx.digest

Good point. Our API only returns JSON so has no need to compile assets.

We use on production for Phoenix umbrella app the multistage Docker build, that is easy to maintain.

ARG ELIXIR_VERSION=1.10.4
ARG ERLANG_VERSION=23.0.3
ARG ALPINE_VERSION=3.12.0
ARG LINUX_VERSION=alpine-$ALPINE_VERSION

#########################
# Stage: deps-assets    #
#########################
FROM hexpm/elixir:$ELIXIR_VERSION-erlang-$ERLANG_VERSION-$LINUX_VERSION as deps-assets

RUN apk --no-cache add \
    ca-certificates \
    nodejs \
    npm \
 && update-ca-certificates --fresh \
 && npm install -g yarn

ENV MIX_ENV=prod \
    MIX_HOME=/opt/mix \
    HEX_HOME=/opt/hex

WORKDIR /app

COPY . .

RUN mix local.rebar --force \
 && mix local.hex --if-missing --force \
 && mix do deps.get --only $MIX_ENV

WORKDIR /app/apps/web/assets

RUN yarn install \
 && yarn build

#########################
# Stage: release        #
#########################

FROM hexpm/elixir:$ELIXIR_VERSION-erlang-$ERLANG_VERSION-$LINUX_VERSION as release

RUN apk --no-cache add git

ENV MIX_ENV=prod \
    MIX_HOME=/opt/mix \
    HEX_HOME=/opt/hex

WORKDIR /app

COPY . .

COPY --from=deps-assets /app/apps/web/priv/static/ /app/apps/web/priv/static/
COPY --from=deps-assets /app/deps/ /app/deps/

RUN mix local.rebar --force \
 && mix local.hex --if-missing --force \
 && mix do deps.get --only $MIX_ENV, deps.compile, phx.digest

RUN mix release \
 && rm -rf /app/deps

#########################
# Stage: production     #
#########################
FROM alpine:$ALPINE_VERSION as production

COPY --from=release /app/_build/prod/rel/app_umbrella ./

RUN chmod +x /app/bin/*
ENV HOME=/app

CMD ["bin/app_umbrella", "start"]

Don't forget to add the .dockerignore file content is a superset of your .gitignore content e.g.

.git
_build
deps
node_modules
priv
tmp
.log

Thanks everyone, this is really appreciated! :heart:

If someone wants to converge those with the best practices in your guides dockerfile and send a PR, it would be very welcome!

For now, I will consider this request fullfilled as we have enough pointers for those who want to move ahead.

@vermaxik This version will have extremely frequent cache-busts due to COPY . ., particularly in an umbrella project which was the focus of this issue.

Something I didn't see described yet - it's super important to any approach that your .dockerignore content is a superset of your .gitignore content.

Something I didn't see described yet - it's super important to any approach that your .dockerignore content is a superset of your .gitignore content.

Thanks a lot @shanesveller for mentioning this, I've updated my comment.

Was this page helpful?
0 / 5 - 0 ratings