Article
Release Mix et image Docker
On va voir ensemble comment créer une Release Mix et une image Docker pour une application écrite en Elixir avec Phoenix Framework grâce à Mix Release.
Vous avez développé votre application avec amour en local depuis un moment et il est temps pour vous de la mettre en ligne. Félicitations !
Vous pouvez utilisez des services comme Fly.io ou Gigalixir si vous voulez des outils clef en main pour le déploiement. Mais si vous voulez le faire sur votre propre serveur, suivez le guide.
Dans cet article et les prochains, nous allons voir :
Comment générer une Release Mix et créer une image Docker ?
Comment utiliser Docker Compose pour notre application Phoenix Framework ?
Comment automatiser la release et la publication de l'image dans une pipeline de CI/CD ?
Tester que votre application se compile en production
Dans un premier temps, vous devez avoir une application qui se compile en production sans erreur. Vous pouvez le vérifier en le faisant en local sur votre machine.
mix deps.get --only prod
MIX_ENV=prod mix compile
MIX_ENV=prod mix assets.deploy
Si tout marche bien, vous n'avez pas d'erreur, sinon vous avez des corrections à faire avant de continuer.
Générer les fichiers nécessaires aux releases
Phoenix peut générer pour vous les fichiers nécessaires à son fonctionnement sous la forme d'une release mix. Dans notre cas, on va lui préciser que l'on veut faire ça avec Docker.
mix phx.gen.release --docker
Il va même vous donner la liste des commandes que l'on pourra plus tard. Si vous avez des warnings concernant votre configuration, vous pouvez les regarder maintenant avant de continuer sur la partie déploiement.
Si vous avez un environnement Docker en local, vous pouvez tester de build votre image.
docker build . -t myapp
Modifier le Dockerfile pour inclure des packages NPM (optionnel)
Si vous avez seulement utilisé Phoenix Framework et Tailwind, vous pouvez passer à la suite ! Mais si vous avez installé des packages NPM dans votre dossier assets
, vous avez une étape supplémentaire.
Tout d'abord, petit rappel pour les plus juniors d'entre nous : si vous installez un package avec NPM, vous devez l'installer avec le flag --save
voir même --save-dev
comme Phoenix Framework inclut un bundler (esbuild).
Ceci étant dit, en local vous n'avez rien à faire, alors pourquoi faut-il une étape supplémentaire ? Tout simplement parce que votre pipeline a besoin de NPM pour installer les dépendances, exactement comme vous l'avez fait en local.
On va simplement rajouter une nouvelle image, avant le builder
qui contient le code suivant.
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
Qu'est-ce que ça veut dire ?
On démarre une image node, qu'on place dans /app, comme les autres.
On copie le dossier assets de notre projet dans ./assets/, soit /app/assets dans l'image.
Et, on y installe les dépendances de production.
Ensuite, il suffit de rajouter un COPY
dans notre builder, juste avant l'appel 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
Ajouter un entrypoint
Le Dockerfile officielle est une bonne base mais elle laisse plusieurs questions en suspens :
Comment l'utiliser avec docker compose ?
Comment lancer nos migrations ?
Pour tout ça, on va utiliser un script qui sera la commande qu'on va exécuter au lancement du conteneur.
Concrètement, on va supprimer la dernière ligne du fichier pour la remplacer par un appel d'un script que l'on va créer juste après.
ENTRYPOINT ["/app/entrypoint.sh"]
Il faut aussi rajouter postgresql-client
dans la liste des dépendances à installer via apt-get
.
Ça nous fait donc cette étape au total :
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"]
Mais qu'y-a-t-il dans le script ?
De manière plutôt transparente pour un script bash, on va attendre que Postgres réponde, puis on va créer la base de données, si elle n'existe pas. Ensuite, on va migrer la base de données et on démarre le serveur.
#!/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
Si vous voulez ajouter des données par défaut à votre projet, il n'y a pas de manière clef en main de le faire pour une Release. Il faudra créer votre propre commande spécifique. Si jamais vous le faites, n'hésitez pas à me remonter les difficultés rencontrées, je pourrais les ajouter à l'article !
J'ai choisi de lancer les migrations systématiquement mais on pourrait imaginer ne pas le vouloir et se connecter manuellement au conteneur Docker pour le faire.
Et c'est tout pour aujourd'hui, on se retrouve très vite pour notre prochain tuto sur la publication de notre image docker sur un registry.
Un dernier refactoring
Après avoir échangé avec un collègue spécialisé sur ces technologies, il m'a conseillé d'utiliser des chemins absolus et non des relatifs dans le Dockerfile pour éviter les erreurs.
On va donc ajouter un ARG
au départ pour choisir notre dossier WORKSPACE
et utiliser cet ARG
à chaque fois qu'on avait avant /app
ou un chemin relatif.
Merci Thomas !
Je vous mets le Dockerfile complet en dessous pour référence.
# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian instead of
# Alpine to avoid DNS resolution issues in production.
#
# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu
# https://hub.docker.com/_/ubuntu?tab=tags
#
#
# This file is based on these images:
#
# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20210902-slim - for the release image
# - https://pkgs.org/ - resource for finding needed packages
# - Ex: hexpm/elixir:1.14.0-erlang-25.0-debian-bullseye-20210902-slim
#
ARG ELIXIR_VERSION=1.14.0
ARG OTP_VERSION=25.0
ARG DEBIAN_VERSION=bullseye-20210902-slim
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
ARG WORKSPACE="/app"
FROM node:lts-slim as builder-node
# prepare build dir
ARG WORKSPACE
WORKDIR ${WORKSPACE}
COPY assets ${WORKSPACE}/assets/
# set build ENV
ENV NODE_ENV=prod
# install npm dependencies
RUN npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
# start a new build stage for elixir
FROM ${BUILDER_IMAGE} as builder
# install build dependencies
RUN apt-get update -y && apt-get install -y build-essential git \
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
# prepare build dir
ARG WORKSPACE
WORKDIR ${WORKSPACE}
# install hex + rebar
RUN mix local.hex --force && \
mix local.rebar --force
# set build ENV
ENV MIX_ENV="prod"
# install mix dependencies
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV
RUN mkdir config
# copy compile-time config files before we compile dependencies
# to ensure any relevant config change will trigger the dependencies
# to be re-compiled.
COPY config/config.exs config/${MIX_ENV}.exs ${WORKSPACE}/config/
RUN mix deps.compile
COPY priv ${WORKSPACE}/priv
COPY lib ${WORKSPACE}/lib
COPY --from=builder-node ${WORKSPACE}/assets ${WORKSPACE}/assets
# compile assets
RUN mix assets.deploy
# Compile the release
RUN mix compile
# Changes to config/runtime.exs don't require recompiling the code
COPY config/runtime.exs ${WORKSPACE}/config/
COPY rel ${WORKSPACE}/rel
RUN mix release
# start a new build stage so that the final image will only contain
# the compiled release and other runtime necessities
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
ARG WORKSPACE
WORKDIR ${WORKSPACE}
RUN chown nobody ${WORKSPACE}
# set runner ENV
ENV MIX_ENV="prod"
# Only copy the final release from the build stage
COPY --from=builder --chown=nobody:root ${WORKSPACE}/_build/${MIX_ENV}/rel/petal_pro ./
COPY entrypoint.sh ./
USER nobody
ENTRYPOINT [ "/app/entrypoint.sh"]