Multi-stage Docker image for phoenix

Btw, did you ever encounter that yarn install needs phoenix and phoenix_html deps that come from mix? I don’t see how you dealt with it.

I modified my package.json to grab phoenix and phoenix_html straight from npm as a regular package.

You can find them at:

And you can use them like normal in your JS, such as: import "phoenix";. Assuming you’re using ES6 style JS.

1 Like

Doh. I had no idea they are available in npm.

Thanks a lot!

No problem. It took a while to wire all of that together, but now that it works, it’s really nice. It’s a setup that will work in both dev and prod using the same docker-compose.yml file because in prod I just wouldn’t use the docker-compose.override.yml file, because in prod nginx would serve the compiled assets instead of running the webpack dev server.

That behavior is dictated by the build args that you can set with ENV variables.

I really wish mix deps.get and mix deps.compile work only with mix.lock (see Mix install in umbrella projects without mix.exs?)

Then we’ll get huge boost on build time for unrelated changes. For example, if you change a mix file to add an option… boom! docker cannot use cache.

Also it’s very cumbersome as you need to add all mix files from all apps in umbrella project. You have 10 umlbreea apps? Then you need to have 10 COPY command to copy all mix files into the right place.

If anyone is interested, here is my latest Docker setup. I hardcoded everything for development. For production I will probably do it differently and build a release. But for development it works just great.

docker/dev/Dockerfile

FROM node:12.4-alpine as webpack

RUN npm install -g yarn

WORKDIR /app/assets

COPY assets/package.json assets/*yarn* ./

RUN apk --update --upgrade add ca-certificates build-base bash \
    && update-ca-certificates --fresh \
    && rm -rf /var/cache/apk/*  \
    && yarn install \
    && apk del build-base

COPY assets .

ENV NODE_ENV="development"

CMD ["tail", "-f", "/dev/null"]

###############################

FROM elixir:1.8-otp-22-alpine as app

WORKDIR /app

RUN apk --update --upgrade add ca-certificates inotify-tools postgresql-client bash \
    && update-ca-certificates --fresh \
    && rm -rf /var/cache/apk/*

COPY ./docker/dev/wait-for-postgres.sh /
COPY mix.* ./

ENV MIX_ENV="dev" \
    MIX_HOME=/opt/mix \
    HEX_HOME=/opt/hex

RUN mix local.hex --force \
    && mix local.rebar --force \
    && mix deps.get \
    && mix deps.compile

CMD ["tail", "-f", "/dev/null"]

docker-compose.yml

version: '3.7'

services:
  postgres:
    image: postgres:11-alpine
    container_name: project_postgres
    restart: always
    ports:
      - 15432:5432
    volumes:
      - ./docker/data/postgres:/var/lib/postgresql/data

  webpack:
    build:
      context: .
      dockerfile: ./docker/dev/Dockerfile
      target: "webpack"
    container_name: project_webpack
    restart: unless-stopped
    command: yarn run watch
    volumes:
      - .:/app
      - static:/app/priv/static

  app:
    build:
      context: .
      dockerfile: ./docker/dev/Dockerfile
      target: "app"
    container_name: project_app
    ports:
      - 4000:4000
    command: tail -f /dev/null
    volumes:
      - .:/app
      - static:/app/priv/static
      - deps:/app/deps
      - build:/app/_build
    depends_on:
      - postgres
      - webpack

volumes:
  static: {}
  deps: {}
  build: {}

docker/dev/wait-for-postgres.sh

#!/bin/bash
# wait-for-postgres.sh

set -e

host="$1"
shift
cmd="$@"

until PGPASSWORD=$POSTGRES_PASSWORD psql -h "$host" -U "postgres" -c '\q'; do
  >&2 echo "Postgres is unavailable - sleeping"
  sleep 1
done

>&2 echo "Postgres is up - executing command"
exec $cmd

And Makefile

.PHONY: app server console sh clean

app:
	docker-compose up --detach --build app

setup: app
	docker-compose exec app /wait-for-postgres.sh postgres mix ecto.setup

server: setup
	docker-compose exec app mix phx.server

console: setup
	docker-compose exec app iex -S mix

sh: setup
	docker-compose exec app bash

clean:
	docker-compose down

Now I can just execute make console or make server and it will start all services and even wait for postgres to be healthy and execute migrations before starting the server on iex.

Thanks @cnck1387 for the helpful tips.

4 Likes

FWIW, I’ve captured a quick tutorial on minimal (Alpine) Multi-Stage Docker image builds with Elixir 1.9 and Phoenix in this Gist: https://gist.github.com/nicbet/102f16359828405ce34ca083976986e1

1 Like

Hi @cnck1387,

I was wondering if you have these dockerfiles and compose files open sourced? I’ve read many blog posts with slightly different approaches and just not sure how to tie it all together for a basic generated phoenix umbrella app (with ecto). I’m trying to get a development environment (editing using vscode remote container plugin) and a mix release driven multi-stage build delivering the .tar.gz release ready for deployment in a container in the same project.