Finally took the jump to docker deployments. Here is the Dockerfile I ended up with

I worked last night to set up a generic Dockerfile I could drop into a elixir project and just build an image.

This may be useful for someone else other than me.

Of course if anyone has any suggestions or tips please drop a note. I generally use elixir/phoenix for API development and as such that is what this Dockerfile was made for.

EDIT(after feedback)
original: https://github.com/jpiepkow/phoenix-docker/blob/8aa7314b1556d973a496cbf3068055f620ec1b27/Dockerfile

new:

6 Likes

Your link gives me a 404…

Sorry about that, didn’t realize I had the repo private. Fixed now!

you shouldn’t need to RUN export you can use the ENV command.

use a .dockerignore file to limit the context docker sends to the daemon, this will also allow you to ignore the _build directory, and not have to delete it.

Running mix release.init should be done in your root directory once, not on each build, because you can use those generated files to override certain things, cookies, etc.

With the new releases in 1.9.1, the apps name will be the name you give it in your mix.exs, so you could potentially standardize on that, and not need the app_name.txt file.

Generally you don’t want to pass in the start command, but just use the ENTRYPOINT command to your build, then you can run it with docker run <hash> <command> giving you a little more flexibility.

2 Likes

Also you want to copy in mix.exs and mix.lock and run get.deps/compile before doing a copy of the whole project. This makes it so if you only have changes in your project code and not in any dependencies they aren’t rebuilt every time you build the image since they (the deps) are in a separate layer.

1 Like

Here is one of my public Dockerfiles. I change them a bit for some specific stuff we do in our app, but this runs on k8s well:

2 Likes

That’s a good point. I think right now, with the whole thing being in one RUN command, it will all be one layer.

2 Likes

Thank you for the helpful feedback!

I agree that I should be using ENV for prod and will make that change.

As for the .dockerignore while I generally agree the idea with this Dockerfile was to drop it in any project and be good to go so for now I think I may keep the removal of _build.

I need to look more into what mix release.init does(first time switching to mix releases)

Can you explain to me more what you mean about the appname. Currently I am reading the name in the mix.exs and storing it so I can reference it in the final image in order to have the path to the built file. Is there a cleaner way I can do this. This is something I spent considerable time trying to figure out.

I like the endpoint advice and may make that change as well(although for my specific use case of not needing to know the app name at start I may keep it for now)

1 Like

here’s a Gist with an example. https://gist.github.com/entone/b0a70be6132c78909a58d2dfb1c49cbb

I would highly recommend the .dockerignore file. If/when you move to CD, and you have some auxiliary files, sending the whole context to the daemon can slow things down pretty significantly.

1 Like

Line #15 here, rel/app, the app at the end is the name you would have in your mix.exs file for the release.

defp releases do
    [
      app: [
        include_executables_for: [:unix],
        applications: [runtime_tools: :permanent]
      ]
    ]
  end

notice the app key

1 Like

Here’s another example that relies heavily on multi-stage builds, https://gist.github.com/entone/43a79cde4199ecb5b6ddd6b2ce6dbd00

With a smallish Elixir/Plug/Cowboy API, this container comes in at about 12MB.

The react frontend is in the repo, but is built into a separate container. Here’s a gist of those files. https://gist.github.com/entone/b93feffe9c46b0a7d6ea279f22484063

this comes in at about 5MB

2 Likes

Thanks a ton this is very helpful!

2 Likes

you wouldn’t need to know the app name, just the command you want to run start, stop, version etc

2 Likes

A few notes :slight_smile:

  1. Use multiple RUN statements
  2. Move COPY as far down you can, then docker can cache some layers
  3. Consider providing MIX_ENV as a build ARG instead of ENV, works the same but will only be available during the build

Note. I’m not a docker expert, take advices with a grain of salt :slight_smile:

1 Like

Makes sense to use as few RUN statements as possible in the release stage though, to save on the number of layers :slight_smile:

2 Likes

Another thought, I see you are creating a new user but are you running the app as that user?

1 Like

There is a nice linter for Dockerfiles:

$ docker run --rm -i hadolint/hadolint < Dockerfile
/dev/stdin:5 SC2002 Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead.
/dev/stdin:5 DL4006 Set the SHELL option -o pipefail before RUN with a pipe in it
/dev/stdin:19 DL3008 Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
/dev/stdin:19 DL3009 Delete the apt-get lists after installing something
/dev/stdin:19 DL3015 Avoid additional packages by specifying `--no-install-recommends`

I do not actually follow it blindly, but in this case, I’d at least listen to DL3009 and DL3015

5 Likes

Thanks for the examples. It was very helpful(mainly in getting everything installed for alpine)

I have since updated the Dockerfile

The image went down from 193mb to 26mb on the test phx API I was using.
This Dockerfile also takes into account caching deps.
This Dockerfile also allows for overriding of the cmd defaulting as start.

I did some experimenting with ENTRYPOINT but could not get down the combination of variable substitution for the directory as well as runtime substitution of the command for various reasons.

I agree that entrypoint is perfect for someone that specifies a release name but I want something I can drop into a repo and build without configuring any release config(generally the default release configs are fine for my projects) so in that case, I need to dynamically navigate with the mix.exs name. In this case, I just specify a default ENV var command that is run and allow substitution on the run command.

All in all, this was a great exercise and I think you tons for the tips. Reducing the image size around x8 and reducing the build time from around 2 minutes to less than 20 seconds with no dependency change is a huge win!

3 Likes

Probably a stupid question, but …

using @entone Dockerfile, everything builds, and I can start the phoenix server using the docker run, all is good. But if I executed with daemon rather than start, it exits immediately, no error etc.

I want to deploy my small app on a mac mini on my home network and have the application restart automatically when docker start.

Any information would be appreciated.

Thanks,
Gary