Deploy a Phoenix app to Microsoft Azure with Docker

After we set up our Phoenix app in the last post, it’s time to get it productive! In this tutorial I’ll guide you through the steps and pitfalls until your Phoenix Framework application is running on Microsoft Azure.

Creating the Docker image

First, we need a production-ready image of our app. I’m using the hello app we created in my last post as an example.
So head over to the official guide “Deploying with Releases“. While you can skip most of it until the last chapture, there are some things we need to do:

Generate a secret with phx.gen.secret:

docker run -it -v ${pwd}/hello:/usr/src/ phoenix:1.5.1 mix phx.gen.secret

This will print out a secret, which we save to somewhere and later set as an environment variable, or, if you don’t want to bother with this for a tutorial, set it as “secret_key_base” in config/prod.secret.exs

In the config/prod.secret.exs file locate the following comment:

#     config :hello, HelloWeb.Endpoint, server: true

Uncomment it, and replace the hellos with your apps name.

Also, as we might not register a domain just for the tutorial, you should set the host to “nil” in your config/prod.exs.

Then, go down to “Containers“, create a Dockerfile in your projects directory (or open the one we created in the last post) and paste the Template from the Guide to there.

Look for this line almost at the end of the Dockerfile:

COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/my_app ./

And change it to your needs:

COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/hello ./

For the build to succeed we need to set the secret as environment variable, so on the top of the Dockerfile add the 2 lines with ARG and ENV:

FROM elixir:1.9.0-alpine AS build

ARG secret
ENV SECRET_KEY_BASE=$secret

This way we can set the secret from command line while building the Docker image. So let’s try:

docker build --build-arg secret=<YourSecretHere> .

If you’re a Windows user, you might get an error like this:

Step 15/26 : RUN npm run --prefix ./assets deploy
---> Running in f89766641336
@ deploy /app/assets
webpack --mode production
/app/assets/node_modules/.bin/webpack: line 1: XSym: not found
/app/assets/node_modules/.bin/webpack: line 2: 0025: not found
/app/assets/node_modules/.bin/webpack: line 3: 04ec15c01fc3c268b6405aedcd29653e: not found
/app/assets/node_modules/.bin/webpack: line 4: ../webpack/bin/webpack.js: not found
npm ERR! file sh
npm ERR! code ELIFECYCLE
npm ERR! errno ENOENT
npm ERR! syscall spawn
npm ERR! @ deploy: webpack --mode production
npm ERR! spawn ENOENT

This is due to a bug in npm. Some users suggest changing this line in the Dockerfile:

RUN npm run --prefix ./assets deploy

to this:

RUN cd assets && npm run deploy && cd ..

If this still doesn’t work, it’s time to finally switch to Linux 😉
Not an option for you? Well, ok, here we go with the dirty tricks…

Comment out the failing line, and move everything from

FROM alpine:3.9 AS app

to another file, i called it DockerfileStage2.

Now we try again:

docker build --build-arg secret=YourSecretHere --tag phoenixbuilder .

This should work fine, but something is missing…
So the next step is:

docker run -it phoenixbuilder sh

which will give you a command promt inside of the Docker image:

/app #

Type this commands:

/app # npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
/app # npm run --prefix ./assets deploy
/app # exit

this will leave you with the manually corrected image which you can use for your further build.

In the DockerfileStage2, change this line:

COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/hello ./

to

COPY --from=phoenixbuilder --chown=nobody:nobody /app/_build/prod/rel/hello ./

and then build the final image:

docker build -f DockerfileStage2 --tag hello .

Congratulations, you survived! Now we can start it up:

docker run -it --rm -p 4000:4000 hello

And access it at http://localhost:4000

If you think we’re now ready to deploy it to Azure, well, uhm, no.
For production, we want to listen to port 80, which is no problem for Docker, just run it like this:

docker run -it --rm -p 80:4000 hello

The problem here: We’re going to deploy it to an Azure Container Instance, and somehow Microsoft thought port mapping isn’t a necessary feature, so all we can do is to open port 80. Unfortunately, we’re not running as root, and therefore can’t open port 80 in Linux by default. But with a little trick in our DockerfileStage2, we can allow the BEAM to do it. We add libcap to ‘apk add’ and use setcap to give the rights. The DockerfileStage2 now looks like this:

FROM alpine:3.9 AS app
RUN apk add --no-cache openssl ncurses-libs libcap

WORKDIR /app

RUN chown nobody:nobody /app

USER nobody:nobody

COPY --from=phoenixbuilder --chown=nobody:nobody /app/_build/prod/rel/hello ./

RUN setcap 'cap_net_bind_service=+eip' /app/erts-10.4.4/bin/beam.smp

ENV HOME=/app

CMD ["bin/hello", "start"]

In your config/prod.secret.exs change the default port 4000 to 80 and build your image again. Finally, we should be ready!

Publishing to Azure

I assume creating an Azure account won’t give you any troubles 🙂
Next thing you’ll need is an Azure container registry, it’s the place to where you’ll upload your Docker images.

In your created Container Registry you’ll find a menu entry “Quick start”, there you should do the the steps with docker login, docker tag and docker push. Of course with replacing hello-world with your apps name 😉
Under Services -> Repositories you’ll now find your pushed image.

Now create a new Container Instance.
As “Image source” you should choose “Azure Container Registry”, which allows you to select your registry and image.
Under Network add port 80.
Under Advanced you need to add an environment variable “SECRET_KEY_BASE” with your previously generated secret.
Then you can create your Instance.

Finally, your Docker Container should be up and running, accessible through either the FQDN chosen while creating the instance, or an IP address which is shown in the Overview of the Container instance.

Changing your Container Instance

The Container Instance webinterface don’t let you do a lot of things, so if you want to change something, you’ll need to do it by command line. Look for this Icon on the top right of your Azure account:

It will open the “Cloud Shell” where you have the “az container create” command. This command is not only used to create a container instance, it’s also used to update an existing one. So for example if you’re going to add SSL to your project, you need to add port 443, which you can do with the following command:

az container create \
    --resource-group <YourResourceGroup> \
    --name <YourInstanceName> \
    --image dwarftech.azurecr.io/<YourImageName>\
    --dns-name-label <YourDnsNameLabel> \
    --ports 80 443

Using an Azure File Share

A very useful Feature if you have to deal with files, e.g. uploaded by users: You can mount an Azure File Share directly into a folder in your container. All you need is an Azure Storage with a file share, and execute the “az container create” again with these 4 new params:

--azure-file-volume-account-name
--azure-file-volume-account-key
--azure-file-volume-share-name picturedwarf
--azure-file-volume-mount-path

Fortunately, this is well documented by Microsoft, so I don’t need to go into any details 😉

Somehow this post got “slightly” longer than I first thought, I hope it’ll help you through the initial struggle when using Phoenix with Docker and Azure 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *