DEV Community

totycro
totycro

Posted on

Running elm in docker

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 which you can use as baseline for your elm project.

Let's check out what's inside!

Dockerfile

There are already some docker images for 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 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 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:

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:

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:

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 and enjoy your encapsulated elm environment!

Discussion (0)