Running elm in docker
Tags: elm, docker
Published: 2019-10-25 13:21:23


So far there doesn't seem to be a nice go-to setup for how to run elm in docker. I would like to have one where it's easy to 
* run an auto-reload development server (elm reactor) and
* start a test-watcher
without needing to think about docker too much.

Basically the environment of the development should be its docker image, but as a developer, you shouldn't have to worry too much about it.

Hence I [created a repo](https://github.com/totycro/elm-docker-template) which you can use as baseline for your elm project.

Let's check out what's inside!

# Dockerfile

There are already [some](https://hub.docker.com/r/wunsh/alpine-elm) docker [images](https://hub.docker.com/r/nacyot/elm-elm) for [elm](https://hub.docker.com/r/codesimple/elm/), which usually either install elm via a package management tool such as `npm` or `cabal`, or build elm from source using a haskell compiler. When using one of them, be careful as to which version of elm they install.

There is even an [image](https://hub.docker.com/r/paullamar3/docker-knowthen-elm) which encorporates a full IDE with plugins preconfigured for use with elm. This is not the *intended* way of using docker, but it might just work out fine for your use case.

For our configuration, let's sticking to the one provided by [codesimple](https://hub.docker.com/r/codesimple/elm/) because its Dockerfile consists of 4 lines and I like simple. Note that it lacks elm-test, so we have to add that manually. It also predetermines the arbitrarily chosen directory `/mnt` as work directory to mount the code in:

```Dockerfile
FROM codesimple/elm:0.19

# NOTE: you actually do need unsafe perm to install elm packages with npm
RUN npm install --unsafe-perm -g elm-test@0.19.0-rev6

# NOTE: we need to set HOME, because elm uses $HOME/.elm to save packages.
#       if this doesn't persist between runs, you get CORRUPT BINARY errors.
ENV HOME /mnt

WORKDIR /mnt
VOLUME /mnt
# NOTE: docker-compose mounts code in /mnt
```


It also fixes a major issue I ran into when running the elm compiler multiple times. Apparently it caches compliation data in the directory `elm-stuff` in the repo root directory (as expected), but also expects installed packages to be present in `$HOME/.elm`. This is a problem when working in docker as in between runs, the data in `$HOME` is not preserved. We fix this issue here using somewhat of a hack: `ENV HOME /mnt`. So we pretend that `/mnt` is actually our home directory and `${HOME}/.elm` ends up in the mounted code directory which persists in between runs.

A nicer fix would be to configure elm to use this directory as cache explicitly, but I'm not aware of such a configuration option.

As a side-effect of this fix, the `.bash_history` also ends up in the mounted directory (since it's saved in `$HOME` as well), so you can use the shell history as expected.


# docker-compose

Moving on to `docker-compose.yml`:

```docker-compose
version: '3.7'
services:
  elm:
    build: .
    volumes:
    - .:/mnt
    ports:
    - 8000
    # we need init to kill elm-test when run with "make test"
    init: true
```

It's very basic and merely sets up the volume mount and exposes port 8000. It also fixes an annoying issue with elm-test:

I found that when running in docker, elm-test ignores `SIGINT`, so you can't kill it via `ctrl-c`. This has to be a problem in the way elm-test is run, since when running elm-test in a shell, it reacts as expected to `ctrl-c`. However by setting `init: true`, docker-compose will make sure that signals are handled and will kill elm-test on `ctrl-c`. Please let me know if you're aware of a more elegant solution to this issue.

# Makefile

At this point, we are basically done and can run commands as we like with docker-compose. Since I'm old already I like to have my commands in a Makefile to be able to run every one conveniently. Since only basic shell commands are executed there you can feel free to convert this to your favorite package script runner :-)

Make sure not to confuse the elm container in docker-compose (named `elm`) with the `elm` command:

```make
all: reactor

RUN_IN_DOCKER = docker-compose run --user $$(id -u)

build:
	docker-compose build elm

compile: build
	${RUN_IN_DOCKER} elm make src/Main.elm

reactor: build
	# NOTE: elm is the container, not the command
	${RUN_IN_DOCKER} -p 8000:8000 elm reactor

test: build
	${RUN_IN_DOCKER} --entrypoint elm-test elm

test-watch: build
	${RUN_IN_DOCKER} --entrypoint elm-test elm --watch

bash: build
	${RUN_IN_DOCKER} --entrypoint bash elm
```

For day to day development, it should suffice to start the dev-server via `make reactor` and have your tests running with `make test-watch`.

You can integrate the command `make test` in your favorite CI pipeline.

So clone/fork the base template from [github](https://github.com/totycro/elm-docker-template) and enjoy your encapsulated elm environment!