Statamic Peak

Article

Mix Release and Docker Image

We will see together how to create a Mix Release and a Docker image for an app written in Elixir with Phoenix framework by using Mix Release.

For a while, you've been putting a lot of love in creating your app, and now comes the time to put it online. Congratulations !

You can use services like Fly.io or Gigalexie if you want turnkey solutions for deployment. But if you want to do it on your own server, follow the guide.

In this article and the next one, we will learn :

  • How to generate a Mix Release and create a Docker image?

  • How to push this image on a Docker registry?

  • How to use Docker Compose in our Phoenix Framework app?

  • How to automatize the images's release and publishing in a CI/CD pipeline?

  • How to automatically deploy our upgraded images?

Check if your app compile in production

At first, your app needs to compile in production without any error. You can check it locally like thisV:

mix deps.get --only prod
MIX_ENV=prod mix compile
MIX_ENV=prod mix assets.deploy

If you don't have errors, move on. In the contrary, you need to make a few corrections before you continue.

Generate the files needed for the release

Phoenix can generate for you the files he needs to work in a mix release. In our case, we will mention we want to do it with Docker.

mix phx.gen.release --docker

You will even get the list of the commands you can use later. If you trigger some warnings during the setup, you can check them now before the deployment.

If you have have a local Docker environment, you can try building your image.

docker build . -t myapp

Modify the Dockerfile to include NPM packages (optionnal)

If you only used Phoenix Framework and Tailwind, you can move on the next step ! But if you installed NPM packages in your assets folder, you need an extra step.

First, quick reminder for the juniors : if you install a package with NPM, you must install it with the --save flag, even sometimes with --save-dev as Phoenix Framework includes a bundler (esbuild).

That being said, you don't have to do anything locally, so why do we need an extra step ? Simply because your pipeline needs NPM to install the dependancies, exactly like we did in local.

We will simply add a new image, before the builder that contains the following code :

FROM node:lts-slim as builder-node

# prepare build dir
WORKDIR /app

COPY assets ./assets/

# set build ENV
ENV NODE_ENV=prod

# install npm dependencies
RUN npm --prefix ./assets ci --progress=false --no-audit --loglevel=error

What does it means ?

We start a node image, that we put in /app, like the others.
We copy the assets folder in our project, either into./assets/, or into /app/assets in the image.

Then we install the prod dependancies.

Finally, we just need to add a COPY in the builder, just before calling RUN mix assets.deploy.

COPY --from=builder-node /app/assets assets

# compile assets
RUN mix assets.deploy # <== lui il est là de base, on va mettre notre COPY juste au dessus

Add an entrypoint

The official Dockerfile is a good start but it raises a few questions :

  • How to use it with docker compose ?

  • How to execute our migrations ?

To do this, we will use a script that will be executed as the container starts.

In practical terms, we will delete the file's last line to replace it with a script call we will create just after.

ENTRYPOINT ["/app/entrypoint.sh"]

We also need to add postgresql-client in out dependencies via apt-get.

We end up with this :

FROM ${RUNNER_IMAGE}

RUN apt-get update -y && apt-get install -y postgresql-client libstdc++6 openssl libncurses5 locales \
  && apt-get clean && rm -f /var/lib/apt/lists/*_*

# Set the locale
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen

ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8

WORKDIR "/app"
RUN chown nobody /app

# set runner ENV
ENV MIX_ENV="prod"

# Only copy the final release from the build stage
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/petal_pro ./
COPY entrypoint.sh ./

USER nobody

ENTRYPOINT [ "/app/entrypoint.sh"]

What's the content of that script ?

Simply, we will wait for Postgres to answer, then create the database if it doesn't exist. Then, we will migrate the database and start the server.

#!/bin/bash
# Docker entrypoint script.

# Wait until Postgres is ready.
while ! pg_isready -q -h $PGHOST -p $PGPORT -U $PGUSER
do
  echo "$(date) - waiting for database to start"
  sleep 2
done

# Create, migrate, and seed database if it doesn't exist.
if [[ -z `psql -Atqc "\\list $PGDATABASE"` ]]; then
  echo "Database $PGDATABASE does not exist. Creating..."
  createdb -E UTF8 $PGDATABASE -l en_US.UTF-8 -T template0
  echo "Database $PGDATABASE created."
fi

/app/bin/migrate
echo "Database $PGDATABASE migrated."

/app/bin/server

If you want to add default datas to your project, they're no specific solutions to make it for a Release. You will have to create your Il faudra créer votre own custom command. If you ever do it, tell me if you encountered any troubles and I will add them to the article !

I choose to launch the migrations systematically but we could also choose not to do it and manually connect to the Docker container to do it.

That's all for today, we will meet soon for our next tutorial abour publishing our docker image on a registry.