Use Docker to Deploy Your Elixir App Anywhere

We’re going to learn how to deploy Elixir using Docker. Docker allows developers to easily package, ship, and run any application as a lightweight, portable, self-sufficient container virtually anywhere. We can use Docker containers with Elixir releases to deploy our Elixir applications on nearly any platform.

I’ll go through the steps to deploy an Elixir Phoenix app using a Docker container.

Overview

Here are the steps we’ll cover to deploy the project:

  1. Create new Elixir Phoenix project
  2. Add Dockerfile to project
  3. Add Docker entrypoint script
  4. Configure Environmental Variables
  5. Add docker-compose file
  6. Build image and deploy

Prerequisites

To follow this guide, make sure you have at least the following applications installed on your machine. In the brackets are the versions with which this guide was written.

  • Elixir (1.11.4-otp-24)
  • Erlang (OTP 24
  • Phoenix (1.5.12)
  • Node.js with npm (11.6.0)
  • Docker (20.10.7)

Make sure to install at least Elixir 1.9.

Create new Elixir Phoenix project

You’ll need an Elixir Phoenix project configured for releases. For a sample project, you can follow our guide: Deploy Your Elixir Phoenix Project with a Release. We also have the finished project up on GitHub as StakNine Elixir Docker.

Add Dockerfile to project

The Phoenix guide provides a Docker template. However, we’re going to use the Docker files and configuration from the Release a Phoenix application with Docker and Postgres blog post. I also relied on some concepts from the Working With Docker section of the Distillery guide.

First, if you are new to Docker, I highly recommend checking out and going through the Docker – Get Started Guide.

Next, add the Dockerfile with instructions about how to build the image, including the release process.

We’re using a base image of elixir:1.11.4-alpine to build and a base image of alpine:3.14.2 to deploy. Find the latest Elixir image here and the latest alpine Linux image here.

# File: docker_phx/Dockerfile
FROM elixir:1.11.4-alpine as build

# install build dependencies
RUN apk add --update git build-base nodejs npm yarn python3

RUN mkdir /app
WORKDIR /app

# install Hex + Rebar
RUN mix do local.hex --force, local.rebar --force

# set build ENV
ENV MIX_ENV=prod

# install mix dependencies
COPY mix.exs mix.lock ./
COPY config config
RUN mix deps.get --only $MIX_ENV
RUN mix deps.compile

# build assets
COPY assets assets
RUN cd assets && npm install && npm run deploy
RUN mix phx.digest

# build project
COPY priv priv
COPY lib lib
RUN mix compile

# build release
# at this point we should copy the rel directory but
# we are not using it so we can omit it
# COPY rel rel
RUN mix release

# prepare release image
FROM alpine:3.14.2 AS app

# install runtime dependencies
RUN apk add --update bash openssl postgresql-client

EXPOSE 4000
ENV MIX_ENV=prod

# prepare app directory
RUN mkdir /app
WORKDIR /app

# copy release to app container
COPY --from=build /app/_build/prod/rel/docker_phx .
COPY entrypoint.sh .
RUN chown -R nobody: /app
USER nobody

ENV HOME=/app
CMD ["bash", "/app/entrypoint.sh"]

If you are using the file as a template in your project, update this line:

COPY --from=build /app/_build/prod/rel/docker_phx .

Add Docker entrypoint script

Then add the Docker entrypoint shell script which makes sure Postgres is ready, runs the migrations, and starts the application.

# File: docker_phx/entrypoint.sh
#!/bin/bash
# docker entrypoint script.

# assign a default for the database_user
DB_USER=${DATABASE_USER:-postgres}

# wait until Postgres is ready
while ! pg_isready -q -h $DATABASE_HOST -p 5432 -U $DB_USER
do
  echo "$(date) - waiting for database to start"
  sleep 2
done

bin="/app/bin/docker_phx"
eval "$bin eval \"DockerPhx.Release.migrate\""
# start the elixir application
exec "$bin" "start"

If you are using the file as a template in your project, update these lines:

bin="/app/bin/docker_phx"
eval "$bin eval \"DockerPhx.Release.migrate\""

Configure Environmental Variables

Add config/docker.env to save each environment variable in a config file. The system environment variables below are for your config, entrypoint.sh, and postgres service.

SECRET_KEY_BASE=REALLY_LONG_SECRET
DATABASE_HOST=db
DATABASE_URL=ecto://postgres:postgres@db/postgres
PORT=4000
HOSTNAME=localhost
POSTGRES_PASSWORD=postgres
LANG=en_US.UTF-8s

You can generate the SECRET_KEY_BASE with the following:

docker_phx $ mix phx.gen.secret
REALLY_LONG_SECRET

Add docker-compose file

Add docker-compose.yml to start and connect your application and database. This file references the image we just built and a separate database image postgres:10-alpine for our db server.

# File docker_phx/docker-compose.yml
version: "3.5"


networks:
  webnet:
    driver: overlay
    attachable: true # Needed in order to run custom commands in the container

services:
  app:
    image: docker_phx:0.1.0
    ports:
      - "80:4000"
    env_file:
      - config/docker.env
    depends_on:
      - db
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
    networks:
      - webnet

  db:
    image: postgres:10-alpine
    deploy:
      replicas: 1
      placement:
        constraints: [node.role == manager]
      restart_policy:
        condition: on-failure
    volumes:
      - "./volumes/postgres:/var/lib/postgresql/data"
    ports:
      - "5432:5432"
    env_file:
      - config/docker.env
    networks:
      - webnet

If you are using the file as a template in your project, update this line to match your project and version:

    image: docker_phx:0.1.0

Before we build our image, we need to edit our config/prod.exs to update the port environment variable to 4000. The docker-compose.yml file has the container port 4000 mapped to the host (or Digital Ocean droplet) port 80.

# config/prod.exs
config :docker_phx, DockerPhxWeb.Endpoint,
  url: [host: "localhost", port: 4000],
  cache_static_manifest: "priv/static/cache_manifest.json"

Build image and deploy

Now that all of our Docker files are ready, it’s time to build our image. You build Docker images with the docker build command. The -t option provides the tagged image name.

If you are on a Mac with M1 Apple Silicon, read How to build x86 Docker images on an M1 Mac. You will need to make some changes to your build process before you deploy to a remote server.

Tag it with the project and version:

docker_phx $ docker build -t docker_phx:0.1.0 .

You’ll see Docker step through each instruction in your Dockerfile, building up your image as it goes. If successful, the build process should end with a message Successfully tagged docker_phx:0.1.0.

Note: You may need to replace node-sass with sass. node-sass has been deprecated.

Now you can start your app locally with docker-compose up which will use your newly tagged image along with the other configuration in docker-compose.yml to build Docker containers (your phoenix app and the database container).

docker_phx $ docker-compose up
WARNING: Some services (app, db) use the 'deploy' key, which will be ignored. Compose does not support 'deploy' configuration - use `docker stack deploy` to deploy to a swarm.
WARNING: The Docker Engine you're using is running in swarm mode.

Compose does not use swarm mode to deploy services to multiple nodes in a swarm. All containers will be scheduled on the current node.

To deploy your application across the swarm, use `docker stack deploy`.

Recreating docker_phx_db_1 ... done
Recreating docker_phx_app_1 ... done
Attaching to docker_phx_db_1, docker_phx_app_1
app_1  | Sun Mar  1 18:23:15 UTC 2020 - waiting for database to start
app_1  | Sun Mar  1 18:23:17 UTC 2020 - waiting for database to start
db_1   |
app_1  | 18:23:22.139 [info] Already up
app_1  | 18:23:28.189 [info] Running DockerPhxWeb.Endpoint with cowboy 2.7.0 at :::4000 (http)
app_1  | 18:23:28.190 [info] Access DockerPhxWeb.Endpoint at http://localhost:4000

Now visit localhost/users on your local machine to see the index of users. NOTE: after we changed the url port, you will now visit localhost instead of localhost:4000

To shutdown your system use Ctrl-C and docker-compose down:

^CGracefully stopping... (press Ctrl+C again to force)
Stopping docker_phx_app_1   ... done
Stopping docker_phx_db_1    ... done

docker_phx $ docker-compose down
WARNING: Some services (app, db) use the 'deploy' key, which will be ignored. Compose does not support 'deploy' configuration - use `docker stack deploy` to deploy to a swarm.
Removing docker_phx_app_1 ... done
Removing docker_phx_db_1  ... done
Removing network docker_phx_webnet

Another way to deploy your is with docker swarm init and docker stack deploy. docker swarm init may not be needed if you already have a swarm running:

docker_phx $ docker swarm init
Error response from daemon: This node is already part of a swarm. Use "docker swarm leave" to leave this swarm and join another one.

docker_phx $ docker stack deploy -c docker-compose.yml docker_phx
Creating network docker_phx_webnet
Creating service docker_phx_app
Creating service docker_phx_db

Now visit localhost/users to see the index of users. You can add a user to test out the site is working properly.

deploy elixir with docker success

Conclusion

Congratulations, you’ve successfully deployed your Elixir apps and database using Docker!! You’ve learned how to build a Docker image for your Elixir project to deploy your application to your local machine.

Now you are ready to deploy your application to any of the available cloud hosting platforms. Check out our tutorial to Deploy Your Elixir App on Digital Ocean!

Want To Know How To Deploy Phoenix Apps Using A single Command?

This brand-new FREE training reveals the most powerful new way to reduce your deployment time and skyrocket your productivity… and truly see your programming career explode off the charts!