Skip to content

Instantly share code, notes, and snippets.

@tfausak
Created January 11, 2017 05:48
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tfausak/c1932ebaeb0cb13a22d4fe5573da1699 to your computer and use it in GitHub Desktop.
Save tfausak/c1932ebaeb0cb13a22d4fe5573da1699 to your computer and use it in GitHub Desktop.
Haskell & Docker
.dockerignore
.gitignore
.stack-work
Dockerfile
README.markdown
/.stack-work/
/*.cabal

Haskell & Docker

This is a simple example of a Haskell application running in a Docker container. The application is a Warp server that responds to every request with "Hello, world!". So, you know, not very exciting. But the entire thing can be built, configured, and run inside a Docker container.

Note that containerization does not prevent you from running this application like normal. In fact, assuming you already have Stack installed you can get it up and running with:

> stack setup
> stack build
> stack exec example

But we are all already familiar with that. Let's see how we can build this project with Docker instead!

Building the container starts from an empty Debian image, gets Stack, installs GHC, builds the dependencies, and finally installs the project. You can do all that with:

> docker build . --tag example

The --tag is optional. If you don't include it, the resulting image will only be identifiable by a hash. Giving it a tag makes it much easier to use.

The container will take a while to build. Once it's done, you can run the server with:

> docker run --interactive --publish-all --tty example

There are a few flags here, so this is what they do:

  • --interactive keeps STDIN open so that you can kill the server with Ctrl-C.
  • --publish-all "publishes" the containers exposed ports to the host. This means that port 80 in the container will be avilable as a random port on the host.
  • --tty allocates a TTY so that STDOUT works.

Once the server is running, you can run figure out which port to connect to with:

> docker ps
CONTAINER ID  IMAGE    COMMAND                 CREATED         STATUS         PORTS                  NAMES
1781a308c625  example  "/bin/sh -c /usr/loca"  28 seconds ago  Up 27 seconds  0.0.0.0:32773->80/tcp  grave_bhabha

For that particular run, pointing your browser to http://localhost:32773 would show a page served by the application.

You should play around with changing the application (Main.hs), the dependencies (package.yaml), and the resolver/compiler (stack.yaml) to see how they affect rebuilds.

Docker's caching works with layers; if you keep a lower layer the same, it will be reused. But any change to a layer causes a rebuild of that layer and everything after it. That means changing the resolver will cause the entire thing to be rebuilt from the ground up.

Note that this Docker container will take over two gigabytes of space on your machine. You can see how much space it use with:

> docker images
REPOSITORY  TAG     IMAGE ID      CREATED         SIZE
debian      8.6     19134a8202e7  4 weeks ago     123.1 MB
example     latest  d1bbc77a84f9  14 minutes ago  2.158 GB

It is possible (and easy!) to make smaller images. This image is made to be easy to understand and quick to develop on. Usually making smaller images means making them less suitable for development. If this image were aiming to be as small as possible, it would be at most a few megabytes larger than the base image (debian:8.6, which is about 123 MB).

Smaller base images are also available! Alpine Linux provides a base image that only takes up 5 MB!

FROM debian:8.6
# Install dependencies.
RUN apt-get update && \
apt-get install --assume-yes curl gcc libgmp-dev make xz-utils zlib1g-dev
# Install Stack.
RUN curl --location https://www.stackage.org/stack/linux-x86_64-static > stack.tar.gz && \
tar xf stack.tar.gz && \
cp stack-*-linux-x86_64-static/stack /usr/local/bin/stack && \
rm -f -r stack.tar.gz stack-*-linux-x86_64-static/stack && \
stack --version
# Install GHC.
WORKDIR /project
COPY stack.yaml /project
RUN stack setup && \
stack exec -- ghc --version
# Install dependencies.
COPY package.yaml /project
RUN stack build --only-dependencies
# Build project.
COPY . /project
RUN stack build --copy-bins --local-bin-path /usr/local/bin
# Run project.
ENV HOST 0.0.0.0
ENV PORT 80
EXPOSE 80
CMD /usr/local/bin/example
import Data.Function ((&))
import qualified Data.ByteString.Lazy.Char8 as ByteString
import qualified Data.Maybe as Maybe
import qualified Data.String as String
import qualified Network.HTTP.Types as HTTP
import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp
import qualified System.Environment as Environment
import qualified Text.Printf as Printf
main :: IO ()
main = do
settings <- getSettings
logStartup settings
Warp.runSettings settings application
getSettings :: IO Warp.Settings
getSettings = do
host <- getHost
port <- getPort
pure (Warp.defaultSettings
& Warp.setHost host
& Warp.setPort port)
getHost :: IO Warp.HostPreference
getHost = do
maybeHost <- Environment.lookupEnv "HOST"
let host = Maybe.fromMaybe "127.0.0.1" maybeHost
pure (String.fromString host)
getPort :: IO Warp.Port
getPort = do
maybePort <- Environment.lookupEnv "PORT"
let port = Maybe.fromMaybe "8080" maybePort
pure (read port)
logStartup :: Warp.Settings -> IO ()
logStartup settings = do
let host = Warp.getHost settings
let port = Warp.getPort settings
Printf.printf "Listening on %s port %d ...\n" (show host) port
application :: Wai.Application
application _request respond =
let
status = HTTP.status200
headers = []
body = ByteString.pack "Hello, world!\n"
in respond (Wai.responseLBS status headers body)
name: example
version: 1.0.0
executables:
example:
main: Main.hs
dependencies:
- base
- bytestring
- http-types
- wai
- warp
ghc-options:
- -Wall
- -threaded
- -rtsopts
- -with-rtsopts=-N
resolver: lts-7.15
@PiotrJustyna
Copy link

Thank you very much for the gist! Your code was of great help when I was setting up a similar docker image on Windows. Here's my blog post about this: http://hack-your-fridge-or-die-trying.blogspot.ie/2017/09/docker-image-to-build-and-run-your.html

@tkvogt
Copy link

tkvogt commented Feb 15, 2021

DockerFile has to be adjusted a little:

FROM debian:8.6

# Install dependencies.
RUN apt-get update && \
  apt-get install --assume-yes curl gcc libgmp-dev make xz-utils zlib1g-dev

# Install Stack.
RUN curl --location https://www.stackage.org/stack/linux-x86_64 > stack.tar.gz && \
  tar xf stack.tar.gz && \
  cp stack-*-linux-x86_64/stack /usr/local/bin/stack && \
  rm -f -r stack.tar.gz stack-*-linux-x86_64/stack && \
  stack --version

# Install GHC.
WORKDIR /project
COPY stack.yaml package.yaml /project/
RUN stack setup && stack exec -- ghc --version

# Install dependencies.
COPY package.yaml /project
RUN stack build --only-dependencies

# Build project.
COPY . /project
RUN stack build --copy-bins --local-bin-path /usr/local/bin

# Run project.
ENV HOST 0.0.0.0
ENV PORT 80
EXPOSE 80
CMD /usr/local/bin/example

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment