Michael Cordell's Blog

Phoenix Development Environment in Docker

The goal of this post is to setup a development environment for a Phoenix web app on macOS in docker. We will shoot to minimize dependencies on the host and also the amount of time manipulating the containers with interactive shells. The end state will be a directory called workdir with the following structure:

.
├── Dockerfile # Working dockerfile for running development commands
├── app # source code for the phoenix app
├── database # volume for the postgres database
├── docker-compose-dev.yml # development docker-compose (not for production)
├── docker-compose.yml # production docker-compose
└── docker-sync.yml # docker-sync for developing the source of the application

Assumed Dependencies

Ruby comes installed in macOS, but you likely will want to use a ruby version manager and to avoid mussing with system gems. A good setup can be found here.

We will start by creating a Dockerfile with node and phoenix on top of the default elixir container. Notice that we are using 1.10.3 of elixir and phoenix 1.5.3. These version numbers can obviously be updated to the latest relevant version.

FROM elixir:1.10.3-alpine

MAINTAINER Michael Cordell <mike@mikecordell.com>

ENV DEBIAN_FRONTEND=noninteractive
ENV MIX_ENV=dev

RUN mix local.hex --force \
 && mix archive.install --force hex phx_new 1.5.3 \
 && apk add --update nodejs \
 && apk add --update npm \
 && apk add --update inotify-tools \
 && mix local.rebar --force

WORKDIR /workdir

We can then use this Dockerfile to create/scaffold the phoenix app Here APP_NAME is a placeholder for whatever you want to all your app. You do not want to use a generic name like “App” here because it is used in the scaffolding of the Phoenix app. :

docker run --volume `pwd`:/workdir -it $(docker build -q .) mix phx.new APP_NAME

This produces the following reminders about how to setup the app, we will get to these in a second.

Phoenix uses an optional assets build tool called webpack
that requires node.js and npm. Installation instructions for
node.js, which includes npm, can be found at http://nodejs.org.

The command listed next expect that you have npm available.
If you don't want webpack, you can re-run this generator
with the --no-webpack option.


We are almost there! The following steps are missing:

    $ cd APP_NAME
    $ cd assets && npm install && node node_modules/webpack/bin/webpack.js --mode development

Then configure your database in config/dev.exs and run:

    $ mix ecto.create

Start your Phoenix app with:

    $ mix phx.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phx.server

First we move the scaffolded folder into it’s position as “app”:

mv APP_NAME app

Now we can install docker-sync which is used to speed up the syncing of the source code of the application into the development container.

gem install docker-sync

With docker-sync installed, we can configure it. Note the name of app-name-sync, you can change it to match your custom app name. This name just needs to match in the following docker-compose-dev.yml.

docker-sync.yml:

version: "2"
options:
  # optional, maximum number of attempts for unison waiting for the success exit
  # status. The default is 5 attempts (1-second sleep for each attempt). Only
  # used in unison.
  max_attempt: 200
syncs:
  app-name-sync:
    # os aware sync strategy, defaults to native_osx under MacOS (except with docker-machine which use unison), and native docker volume under linux
    # remove this option to use the default strategy per os or set a specific one
    sync_strategy: 'native_osx'
    # which folder to watch / sync from - you can use tilde, it will get expanded.
    # the contents of this directory will be synchronized to the Docker volume with the name of this sync entry ('shortexample-sync' here)
    src: './app/'

    host_disk_mount_mode: 'cached' # see https://docs.docker.com/docker-for-mac/osxfs-caching/#cached
    # other unison options can also be specified here, which will be used when run under osx,
    # and ignored when run under linux

Now we can setup the docker-compose file for development. Having a separate docker-compose file allows for the custom configuration of the docker-sync volume while keeping it separate from the production configuration. In this configuration, we are pairing the phoenix application with a Postgres container and volume Stored at ./database within our working dir. .

docker-compose-dev.yml:

version: "2"

services:
  web:
    build:
      context: ./app/
      dockerfile: ../Dockerfile
    working_dir: /app
    ports:
      - "4000:4000"
    command: mix phx.server
    environment:
      - MIX_ENV=dev
      - PORT=4000
      - PG_HOST=db
      - PG_USERNAME=postgres
      - PG_PASSWORD=postgres
    volumes:
      - app-name-sync:/app:nocopy # nocopy is important
    links:
      - db
  db:
    image: postgres:12.3-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_HOST=db
    volumes:
      - ./database:/var/lib/postgresql/data

volumes:
  app-name-sync:
    external: true

Before we can start using the docker-compose setup we need to configure the development configuration in the application source to point at the docker configuration. This is done by modifying the app/config/dev.exs to use the docker username, password, and host environment variables:

diff --git a/config/dev.exs b/config/dev.exs
index 6d184c3..332d83a 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -2,10 +2,10 @@ use Mix.Config

 # Configure your database
 config :app_name, AppName.Repo,
-  username: "postgres",
-  password: "postgres",
+  username: System.get_env("PG_USERNAME"),
+  password: System.get_env("PG_PASSWORD"),
+  hostname: System.get_env("PG_HOST"),
   database: "app_name_dev",
-  hostname: "localhost",
   show_sensitive_data_on_connection_error: true,
   pool_size: 10

Now we can run database mix commands and create the database:

docker-compose -f docker-compose-dev.yml run web mix ecto.create

Finally, we need to setup up the front end dependencies and webpack. We cheat a bit here an use the interactive shell:

docker-compose -f docker-compose-dev.yml run web /bin/sh

In the web container:

cd /app/assets
npm install && node node_modules/webpack/bin/webpack.js --mode development
exit

To clean up a bit, lets commit this initial source:

cd app
git init .
git commit -m "Initial commit"
cd ..

Lets test out our setup by bringing up the docker-compose-dev file:

docker-compose -f docker-compose-dev.yml up

Navigate to http://localhost:4000

Open the file app/lib/app_name_web/templates/page/index.html and modify some visible text. For example, changing the paragraph tag from: Peace-of-mind from prototype to production to Running in docker.

Save the file, your browser should live reload reflecting your change. You now have a working development in environment for phoenix!