├── encrypted.txt ├── cabal.project ├── .gitignore ├── Makefile ├── .dockerignore ├── docker.cabal.config ├── docker-haskell-example.cabal ├── src └── Main.hs ├── Dockerfile ├── EMBEDDING.md └── README.md /encrypted.txt: -------------------------------------------------------------------------------- 1 | U2FsdGVkX1/vH2+c57uP/TvD7ErdUrbUQkGNBFJpfrCkAufS9R2Z3fxnG5Zj3djE 2 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | index-state: 2019-06-17T09:52:09Z 2 | with-compiler: ghc-8.4.4 3 | packages: . 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist-newstyle/ 2 | .ghc.environment.* 3 | cabal.project.local 4 | README.md 5 | EMBEDDING.md 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build-docker : 2 | docker build --build-arg EXECUTABLE=docker-haskell-example --tag docker-haskell-example:latest . 3 | 4 | run-docker : 5 | docker run -ti -e PASSWORD=HaskellCurry --publish 8000:8000 docker-haskell-example:latest 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # docker 2 | .dockerignore 3 | Dockerfile 4 | 5 | # git 6 | .git/ 7 | .gitignore 8 | 9 | # cabal-install 10 | dist-newstyle/ 11 | cabal.project.local 12 | 13 | # my editor(s) 14 | *.swp 15 | *.swo 16 | *.swn 17 | *~ 18 | 19 | # documentation 20 | README.md 21 | -------------------------------------------------------------------------------- /docker.cabal.config: -------------------------------------------------------------------------------- 1 | verbose: normal +nowrap +markoutput 2 | remote-build-reporting: anonymous 3 | write-ghc-environment-files: always 4 | jobs: 4 5 | 6 | remote-repo-cache: /cabal/packages 7 | logs-dir: /cabal/logs 8 | world-file: /cabal/world 9 | extra-prog-path: /cabal/bin 10 | symlink-bindir: /cabal/bin 11 | build-summary: /cabal/logs/build.log 12 | store-dir: /cabal/store 13 | 14 | install-dirs user 15 | prefix: /cabal 16 | 17 | repository hackage.haskell.org 18 | url: http://hackage.haskell.org/ 19 | -------------------------------------------------------------------------------- /docker-haskell-example.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.4 2 | name: docker-haskell-example 3 | version: 0 4 | 5 | executable docker-haskell-example 6 | default-language: Haskell2010 7 | hs-source-dirs: src/ 8 | main-is: Main.hs 9 | build-depends: 10 | , base ^>=4.11 11 | , base16-bytestring ^>=0.1.1.6 12 | , base64-bytestring ^>=1.0.0.2 13 | , bytestring ^>=0.10.8.2 14 | , file-embed-lzma ^>=0 15 | , HsOpenSSL ^>=0.11.4.16 16 | , servant ^>=0.16 17 | , servant-server ^>=0.16 18 | , unix ^>=2.7.2.2 19 | , warp ^>=3.2.27 20 | -------------------------------------------------------------------------------- /src/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE TemplateHaskell #-} 4 | module Main (main) where 5 | 6 | import Control.Monad (when) 7 | import Servant 8 | import System.IO (stdout, hFlush) 9 | import qualified Network.Wai.Handler.Warp as Warp 10 | import System.Posix.Env.ByteString (getEnv) 11 | import Data.ByteString (ByteString) 12 | import FileEmbedLzma (embedByteString) 13 | import Data.ByteString.Base64 (decodeLenient) 14 | import OpenSSL (withOpenSSL) 15 | import OpenSSL.EVP.Digest (pkcs5_pbkdf2_hmac_sha1) 16 | import OpenSSL.EVP.Cipher (getCipherByName, CryptoMode(Decrypt), cipherBS, getCipherNames) 17 | 18 | import qualified Data.ByteString.Base16 as Base16 19 | import qualified Data.ByteString as BS 20 | import qualified Data.ByteString.Char8 as BS8 21 | 22 | encrypted :: ByteString 23 | encrypted = $(embedByteString "encrypted.txt") 24 | 25 | encrypted' :: ByteString 26 | encrypted' = decodeLenient encrypted 27 | 28 | iters :: Int 29 | iters = 100000 30 | 31 | extract 32 | :: ByteString -- ^ password 33 | -> ByteString -- ^ encrypted data 34 | -> IO ByteString -- ^ decrypted data 35 | extract password bs0 = do 36 | when (BS.length bs0 < 16) $ fail "Too small input" 37 | 38 | let (magic, bs1) = BS.splitAt 8 bs0 39 | (salt, enc) = BS.splitAt 8 bs1 40 | 41 | when (magic /= "Salted__") $ fail "No Salted__ header" 42 | -- BS8.putStrLn $ "salt=" <> Base16.encode salt 43 | 44 | let (key, iv) = BS.splitAt 32 45 | $ pkcs5_pbkdf2_hmac_sha1 password salt iters 48 46 | 47 | -- BS8.putStrLn $ "key=" <> Base16.encode key 48 | -- BS8.putStrLn $ "iv= " <> Base16.encode iv 49 | 50 | cipher <- getCipherByName "aes-256-cbc" >>= maybe (fail "no cipher") return 51 | plain <- cipherBS cipher key iv Decrypt enc 52 | 53 | return plain 54 | 55 | type ExampleAPI = Get '[JSON] [String] 56 | 57 | exampleAPI :: Proxy ExampleAPI 58 | exampleAPI = Proxy 59 | 60 | exampleServer :: String -> Server ExampleAPI 61 | exampleServer msg = return ["hello", "world", msg] 62 | 63 | main :: IO () 64 | main = withOpenSSL $ do 65 | password <- getEnv "PASSWORD" >>= maybe (fail "PASSWORD not set") return 66 | plain <- extract password encrypted' 67 | putStrLn "http://localhost:8000" 68 | hFlush stdout 69 | Warp.run 8000 $ serve exampleAPI $ exampleServer $ BS8.unpack plain 70 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # BUILDER 2 | ############################################################################## 3 | 4 | FROM ubuntu:18.04 AS builder 5 | LABEL author="Oleg Grenrus " 6 | 7 | # A path we work in 8 | WORKDIR /build 9 | 10 | # Update APT 11 | RUN apt-get -yq update && apt-get -yq upgrade 12 | 13 | # hvr-ppa, provides GHC and cabal-install 14 | RUN apt-get -yq --no-install-suggests --no-install-recommends install \ 15 | software-properties-common \ 16 | apt-utils \ 17 | && apt-add-repository -y "ppa:hvr/ghc" 18 | 19 | # Locales 20 | # - UTF-8 is good 21 | RUN apt-get -yq --no-install-suggests --no-install-recommends install \ 22 | locales 23 | 24 | RUN locale-gen en_US.UTF-8 25 | ENV LANG=en_US.UTF-8 26 | ENV LANGUAGE=en_US:en 27 | ENV LC_ALL=en_US.UTF-8 28 | 29 | # Some what stable dependencies 30 | # - separately, mostly to spot ghc and cabal-install 31 | RUN apt-get -yq --no-install-suggests --no-install-recommends install \ 32 | cabal-install-2.4 \ 33 | ghc-8.4.4 \ 34 | ghc-8.6.5 \ 35 | git 36 | 37 | # More dependencies, all the -dev libraries 38 | # - some basic collection of often needed libs 39 | # - also some dev tools 40 | RUN apt-get -yq --no-install-suggests --no-install-recommends install \ 41 | build-essential \ 42 | ca-certificates \ 43 | curl \ 44 | git \ 45 | libgmp-dev \ 46 | liblapack-dev \ 47 | liblzma-dev \ 48 | libpq-dev \ 49 | libssl-dev \ 50 | libyaml-dev \ 51 | netbase \ 52 | openssh-client \ 53 | pkg-config \ 54 | zlib1g-dev 55 | 56 | # Set up PATH 57 | ENV PATH=/cabal/bin:/opt/ghc/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 58 | 59 | # cabal-install configuration 60 | # - we'll be in better control of the build environment, than with default config. 61 | COPY docker.cabal.config /build/cabal.config 62 | ENV CABAL_CONFIG /build/cabal.config 63 | 64 | # Update cabal-install database 65 | RUN cabal v2-update 66 | 67 | # Install cabal-plan 68 | # - we'll need it to find build artifacts 69 | # - note: actual build tools ought to be specified in build-tool-depends field 70 | RUN cabal v2-install cabal-plan --constraint='cabal-plan ^>=0.5' --constraint='cabal-plan +exe' 71 | 72 | # Add a .cabal file to build environment 73 | # - it's enough to build dependencies 74 | COPY *.cabal cabal.project /build/ 75 | 76 | # Build package dependencies first 77 | # - beware of https://github.com/haskell/cabal/issues/6106 78 | RUN cabal v2-build -v1 --dependencies-only all 79 | 80 | # Add rest of the files into build environment 81 | # - remember to keep .dockerignore up to date 82 | COPY . /build 83 | 84 | # An executable to build 85 | ARG EXECUTABLE 86 | 87 | # Check that ARG is set up 88 | RUN if [ -z "$EXECUTABLE" ]; then echo "ERROR: Empty $EXECUTABLE"; false; fi 89 | 90 | # BUILD!!! 91 | RUN cabal v2-build -v1 exe:$EXECUTABLE 92 | 93 | # Copy build artifact to known directory 94 | # - todo arg 95 | RUN mkdir -p /build/artifacts && cp $(cabal-plan list-bin $EXECUTABLE) /build/artifacts/ 96 | 97 | # Make a final binary a bit smaller 98 | RUN strip /build/artifacts/$EXECUTABLE; done 99 | 100 | # Small debug output 101 | RUN ls -lh /build/artifacts 102 | 103 | # DEPLOYMENT IMAGE 104 | ############################################################################## 105 | 106 | FROM ubuntu:18.04 107 | LABEL author="Oleg Grenrus " 108 | 109 | # Dependencies 110 | # - no -dev stuff 111 | # - cleanup apt stuff after installation 112 | RUN apt-get -yq update && apt-get -yq --no-install-suggests --no-install-recommends install \ 113 | ca-certificates \ 114 | curl \ 115 | libgmp10 \ 116 | liblapack3 \ 117 | liblzma5 \ 118 | libpq5 \ 119 | libssl1.1 \ 120 | libyaml-0-2 \ 121 | netbase \ 122 | openssh-client \ 123 | zlib1g \ 124 | && apt-get clean \ 125 | && rm -rf /var/lib/apt/lists/* 126 | 127 | # Working directory 128 | WORKDIR /app 129 | 130 | # Expose port 131 | EXPOSE 8000 132 | 133 | # Inherit the executable argument 134 | ARG EXECUTABLE 135 | 136 | # Copy build artifact from a builder stage 137 | COPY --from=builder /build/artifacts/$EXECUTABLE /app/$EXECUTABLE 138 | 139 | # ARG env isn't preserved, so we make another ENV 140 | ENV EXECUTABLE_ $EXECUTABLE 141 | 142 | RUN env 143 | RUN ls /app 144 | 145 | # Set up a default command to run 146 | ENTRYPOINT /app/${EXECUTABLE_} 147 | -------------------------------------------------------------------------------- /EMBEDDING.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Embedding secret data into Docker images 3 | author: Oleg Grenrus 4 | tags: engineering 5 | --- 6 | 7 | In [Multi-stage docker build of Haskell webapp](2019-07-04-docker-haskell-example.html) 8 | blog post I briefly mentioned `data-files`. They are problematic. 9 | A simpler way is to use e.g. [`file-embed-lzma`](https://hackage.haskell.org/package/file-embed-lzma) 10 | or similar functionality to *embed data* into the final binary. 11 | 12 | You can also embed *secret data* if you first *encrypt* it. This would reduce 13 | the pain when dealing with (large) secrets. I personally favor configuration 14 | (of running Docker containers) through environment variables. Injecting extra 15 | data into containers is inelegant: another way to "configure" running 16 | container. 17 | 18 | In this blog post, I'll show that dealing with encrypted data in Haskell 19 | is not too complicated. 20 | The code is in the [same repository](https://github.com/phadej/docker-haskell-example) 21 | as the previous post. 22 | This post is based on 23 | [Tutorial: AES Encryption and Decryption with OpenSSL](https://eclipsesource.com/blogs/2017/01/17/tutorial-aes-encryption-and-decryption-with-openssl/), 24 | but is updated and adapted for Haskell. 25 | 26 |
27 | 28 | Encrypting: OpenSSL Command Line 29 | -------------------------------- 30 | 31 | To encrypt a plaintext using AES with OpenSSL, the `enc` command is used. The 32 | following command will prompt you for a password, encrypt a file called 33 | `plaintext.txt` and Base64 encode the output. The output will be written to 34 | `encrypted.txt`. 35 | 36 | ```bash 37 | openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000 -base64 -md sha1 -in plaintext.txt -out encrypted.txt 38 | ``` 39 | 40 | This will result in a different output each time it is run. This is because a 41 | different (random) salt is used. The *Salt* is written as part of the output, 42 | and we will read it back in the next section. 43 | I used `HaskellCurry` as a password, and placed an encrypted file in the repository. 44 | 45 | Note that we use `-pbkdf2` flag. It's available since OpenSSL 1.1.1, 46 | which *is* available in Ubuntu 18.04 at the time of writing. 47 | Update your systems! We use 100000 iterations. 48 | 49 | The choice of SHA1 digest is done because 50 | [`pkcs5_pbkdf2_hmac_sha1`](https://hackage.haskell.org/package/HsOpenSSL-0.11.4.16/docs/OpenSSL-EVP-Digest.html#v:pkcs5_pbkdf2_hmac_sha1) 51 | exists directly in `HsOpenSSL`. 52 | We will use it to derive key and IV from a password in Haskell. 53 | Alternatively, you could use `-p` flag, so 54 | `openssl` prints the used Key and IV and provide these to the running 55 | service. 56 | 57 | Decrypting: OpenSSL Command Line 58 | -------------------------------- 59 | 60 | To decrypt file on command line, we'll use `-d` option: 61 | 62 | ```bash 63 | openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000 -base64 -md sha1 -d -in encrypted.txt 64 | ``` 65 | 66 | This command is useful to check "what's there". Next, the Haskell version. 67 | 68 | Decrypting: Haskell 69 | ------------------- 70 | 71 | To decrypt the output of an AES encryption (aes-256-cbc) we will use the 72 | [`HsOpenSSL`](https://hackage.haskell.org/package/HsOpenSSL) library. 73 | Unlike the command line, each step must be explicitly performed. 74 | Luckily, it's a lot nice that using C. There 6 steps: 75 | 76 | 1. Embed a file 77 | 2. Decode Base64 78 | 3. Extract the salt 79 | 4. Get a password 80 | 5. Compute the key and initialization vector 81 | 6. Decrypt the ciphertext 82 | 83 |

Embed a file

84 | 85 | To embed file we use *Template Haskell*, 86 | [`embedByteString`](https://hackage.haskell.org/package/file-embed-lzma-0/docs/FileEmbedLzma.html#v:embedByteString) 87 | from `file-embed-lzma` library. 88 | 89 | ```haskell 90 | {-# LANGUAGE TemplateHaskell #-} 91 | 92 | import Data.ByteString (ByteString) 93 | import FileEmbedLzma (embedByteString) 94 | 95 | encrypted :: ByteString 96 | encrypted = $(embedByteString "encrypted.txt") 97 | ``` 98 | 99 |

Decode Base64

100 | 101 | Decoding Base64 is an one-liner in Haskell. 102 | We use [`decodeLenient`](https://hackage.haskell.org/package/base64-bytestring-1.0.0.2/docs/Data-ByteString-Base64.html#v:decodeLenient) 103 | because we are quite sure input is valid. 104 | 105 | ```haskell 106 | import Data.ByteString.Base64 (decodeLenient) 107 | 108 | encrypted' :: ByteString 109 | encrypted' = decodeLenient encrypted 110 | ``` 111 | 112 | Note: `HsOpenSSL` can also handle Base64, but doesn't seem to provide 113 | lenient variant. `HsOpenSSL` throws exceptions on errors. 114 | 115 |

Extract the salt

116 | 117 | Once we have decoded the cipher, we can read the salt. 118 | The Salt is identified by the 8 byte header (`Salted__`), 119 | followed by the 8 byte salt. 120 | We start by ensuring the header exists, and then we extract the following 8 bytes: 121 | 122 | ```haskell 123 | extract 124 | :: ByteString -- ^ password 125 | -> ByteString -- ^ encrypted data 126 | -> IO ByteString -- ^ decrypted data 127 | extract password bs0 = do 128 | when (BS.length bs0 < 16) $ fail "Too small input" 129 | 130 | let (magic, bs1) = BS.splitAt 8 bs0 131 | (salt, enc) = BS.splitAt 8 bs1 132 | 133 | when (magic /= "Salted__") $ fail "No Salted__ header" 134 | 135 | ... 136 | ``` 137 | 138 |

Get a password

139 | 140 | We use `unix` package, 141 | and [`System.Posix.Env.ByteString.getEnv`](https://hackage.haskell.org/package/unix-2.7.2.2/docs/System-Posix-Env-ByteString.html#v:getEnv) 142 | to get environment variable as `ByteString` directly. 143 | The program will run in Docker in Linux: depending on `unix` is not a problem. 144 | 145 | ```haskell 146 | {-# LANGUAGE OverloadedStrings #-} 147 | 148 | import System.Posix.Env.ByteString (getEnv) 149 | import OpenSSL (withOpenSSL) 150 | 151 | main :: IO () 152 | main = withOpenSSL $ do 153 | password <- getEnv "PASSWORD" >>= maybe (fail "PASSWORD not set") return 154 | ... 155 | ``` 156 | 157 | We also initialize the OpenSSL library using [`withOpenSSL`](https://hackage.haskell.org/package/HsOpenSSL-0.11.4.16/docs/OpenSSL.html#v:withOpenSSL). 158 | 159 |

Compute the key and initialization vector

160 | 161 | Once we have extracted the salt, we can use the salt and password to generate 162 | the Key and Initialization Vector (IV). To determine the Key and IV from the 163 | password we use the 164 | [`pkcs5_pbkdf2_hmac_sha1`](https://hackage.haskell.org/package/HsOpenSSL-0.11.4.16/docs/OpenSSL-EVP-Digest.html#v:pkcs5_pbkdf2_hmac_sha1) 165 | function. PBKDF2 (Password-Based Key Derivation Function 2) is 166 | a key derivation function. We (as `openssl`) derive both key and IV simultaneously: 167 | 168 | ```haskell 169 | import OpenSSL.EVP.Digest (pkcs5_pbkdf2_hmac_sha1) 170 | 171 | iters :: Int 172 | iters = 100000 173 | 174 | ... 175 | let (key, iv) = BS.splitAt 32 176 | $ pkcs5_pbkdf2_hmac_sha1 password salt iters 48 177 | ... 178 | ``` 179 | 180 |

Decrypting the ciphertext

181 | 182 | With the Key and IV computed, and the ciphertext decoded from Base64, we are now 183 | ready to decrypt the message. 184 | 185 | ```haskell 186 | import OpenSSL.EVP.Cipher (getCipherByName, CryptoMode(Decrypt), cipherBS) 187 | 188 | ... 189 | cipher <- getCipherByName "aes-256-cbc" 190 | >>= maybe (fail "no cipher") return 191 | plain <- cipherBS cipher key iv Decrypt enc 192 | ... 193 | ``` 194 | 195 | Conclusion 196 | ---------- 197 | 198 | In this post we embedded an encrypted file into Haskell application, 199 | which is then decrypted at run time. The complete copy of the code is 200 | at [same repository](https://github.com/phadej/docker-haskell-example), 201 | and changes done for this post are visible in 202 | [a pull request](https://github.com/phadej/docker-haskell-example/pull/1). 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Multi-stage docker build of Haskell webapp 3 | author: Oleg Grenrus 4 | tags: engineering 5 | --- 6 | 7 | Since Docker 17.05, there is support for [multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/). 8 | This is an example and tutorial for using it to build simple Haskell webapp. 9 | The setup is simple: single `Dockerfile`, yet the resulting docker image 10 | is only megabytes large. 11 | 12 | Essentially, I read through [Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) 13 | and made a `Dockerfile`. 14 | 15 | *A word of warning:* If you think Nix is the tool to use, I'm fine with that. 16 | But there's nothing for you in this tutorial. 17 | This is an opinionated setup: Ubuntu and `cabal-install`'s nix-style build. 18 | Also all non-Haskell dependencies are assumed to be avaliable for install 19 | through `apt-get`. If that's not true in your case, maybe you should 20 | check Nix. 21 | 22 | The files are on [GitHub: phadej/docker-haskell-example](https://github.com/phadej/docker-haskell-example). 23 | I refer to files by names, not paste them here. 24 | 25 | Assuming you have docker tools installed, there are seven steps to build a docker image: 26 | 27 | 1. Write your application. Any web-app would do. The assumptions are that the app 28 | - listens at port 8000 29 | - doesn't daemonize itself 30 | 31 | I use a minimal servant app: 32 | [`docker-haskell-example.cabal`](https://github.com/phadej/docker-haskell-example/blob/master/docker-haskell-example.cabal) and 33 | [`Main.hs`](https://github.com/phadej/docker-haskell-example/blob/master/src/Main.hs). 34 | If you want to learn about servant, [its tutorial](https://haskell-servant.readthedocs.io/en/stable/tutorial/index.html) is a good starting point. 35 | 36 | 2. Write `cabal.project` containing at least 37 | 38 | ```plain 39 | index-state: 2019-06-17T09:52:09Z 40 | with-compiler: ghc-8.4.4 41 | packages: . 42 | ``` 43 | 44 | - Pinning `index-state` makes builds reproducible enough 45 | - `with-compiler` select the compiler so it's not the default `ghc` 46 | 47 | 3. Add [`.dockerignore`](https://github.com/phadej/docker-haskell-example/blob/master/.dockerignore). 48 | The more stuff you can ignore, the better. Less things to copy to docker build context. 49 | Less things would invalidate docker cache. 50 | Especially hidden files are not hidden from docker, like editors' temporary files. 51 | I hide `.git` directory. If you want to burn git-hash look at known issues section. 52 | 53 | 4. Add [`Dockerfile`](https://github.com/phadej/docker-haskell-example/blob/master/Dockerfile) 54 | and [`docker.cabal.config`](https://github.com/phadej/docker-haskell-example/blob/master/docker.cabal.config). 55 | `docker.cabal.config` is used in `Dockerfile`. 56 | In most cases you don't need to edit `Dockerfile`. You need, if you need some additional system dependencies. 57 | The next step will tell, if you need something. 58 | 59 | 5. Build an image with 60 | 61 | ```bash 62 | docker build --build-arg EXECUTABLE=docker-haskell-example --tag docker-haskell-example:latest . 63 | ``` 64 | 65 | If it fails, due missing library, see next section. You'll need to edit 66 | `Dockerfile`, and iterate until you get a successful build. 67 | 68 | 6. After successful build, you can run the container locally 69 | 70 | ```bash 71 | docker run -ti --publish 8000:8000 docker-haskell-example:latest 72 | ``` 73 | 74 | This step is important, to test that all runtime dependencies are there. 75 | 76 | 7. And try it from another terminal 77 | 78 | ```bash 79 | curl -D - localhost:8000 80 | ``` 81 | 82 | It should respond something like: 83 | 84 | ```plain 85 | HTTP/1.1 200 OK 86 | Transfer-Encoding: chunked 87 | Date: Thu, 04 Jul 2019 16:15:37 GMT 88 | Server: Warp/3.2.27 89 | Content-Type: application/json;charset=utf-8 90 | 91 | ["hello","world"] 92 | ``` 93 | 94 | What happens in the Dockerfile 95 | ------------------------------ 96 | 97 | The `Dockerfile` is written with a monorepo setup in mind. In other words 98 | setup, where you could build different docker images from a single repository. 99 | That explains the `--build-arg EXECUTABLE=` in a `docker build` command. 100 | It's also has some comments explaining *why* particular steps are done. 101 | 102 | There are two stages in the `Dockerfile`, *builder* and *deployment*. 103 | 104 |

Builder stage

105 | 106 | In builder stage we install all **build dependencies**, 107 | separating them into different `RUN`s, so we could avoid 108 | cache invalidation as much as possible. 109 | A general rule: *Often changing things have to installed latter*. 110 | 111 | We install few dependencies from Ubuntu's package repositories. 112 | That list is something you'll need to edit once in a while. 113 | The assumption is that all non-Haskell stuff comes from there 114 | (or some PPA). There's also a corresponding list in *deployment* stage, there 115 | we install only non-dev versions. 116 | 117 | ```dockerfile 118 | # More dependencies, all the -dev libraries 119 | # - some basic collection of often needed libs 120 | # - also some dev tools 121 | RUN apt-get -yq --no-install-suggests --no-install-recommends install \ 122 | build-essential \ 123 | ca-certificates \ 124 | curl \ 125 | git \ 126 | libgmp-dev \ 127 | liblapack-dev \ 128 | liblzma-dev \ 129 | libpq-dev \ 130 | libyaml-dev \ 131 | netbase \ 132 | openssh-client \ 133 | pkg-config \ 134 | zlib1g-dev 135 | ``` 136 | 137 | At some point we reach a point, where we add `*.cabal` file. This is something 138 | you might need to edit as well, if you have multiple cabal files in different 139 | directories. 140 | 141 | ```dockerfile 142 | # Add a .cabal file to build environment 143 | # - it's enough to build dependencies 144 | COPY *.cabal cabal.project /build/ 145 | ``` 146 | 147 | We only add these, so we can build dependencies. 148 | 149 | 150 | 151 | ```dockerfile 152 | # Build package dependencies first 153 | # - beware of https://github.com/haskell/cabal/issues/6106 154 | RUN cabal v2-build -v1 --dependencies-only all 155 | ``` 156 | 157 | and their cache 158 | won't be violated by changes in the actual implementation of the webapp. 159 | This is common idiom in `Dockerfile`s. 160 | [Issue 6106](https://github.com/haskell/cabal/issues/6106) might 161 | be triggered if you vendor some dependencies. In that case 162 | change the build command to 163 | 164 | ```dockerfile 165 | RUN cabal v2-build -v1 --dependencies-only some-dependencies 166 | ``` 167 | 168 | listing as many dependencies (e.g. `servant`, `warp`) as possible. 169 | 170 | After dependencies are built, the rest of the source files are added 171 | and the executables are built, stripped, and moved to known location 172 | out of `dist-newstyle` guts. 173 | 174 |

Deployment image

175 | 176 | The deployment image is slick. We pay attention and don't install 177 | development dependencies anymore. In other words we install only **runtime dependencies**. 178 | E.g. we install `libgmp10`, not `libgmp-dev`. 179 | I also tend to install `curl` and some other cli tool to help debugging. 180 | In deployment environments where you can shell into the running containers, it 181 | helps if there's something you can do. That feature is useful to debug network 182 | problems for example. 183 | 184 | The resulting image is not the smallest possible, 185 | but it's not huge either: 186 | 187 | ``` 188 | REPOSITORY TAG SIZE 189 | docker-haskell-example latest 137MB 190 | ``` 191 | 192 | Cold build is slow. 193 | Rebuilds are reasonably fast, 194 | if you don't touch `.cabal` or `cabal.project` files. 195 | 196 | Known issues 197 | ------------ 198 | 199 | - If you have `data-files`, situation is tricky: 200 | Consider using 201 | [`file-embed-lzma`](https://hackage.haskell.org/package/file-embed-lzma) 202 | or [`file-embed`](https://hackage.haskell.org/package/file-embed) 203 | packages. I.e. avoid `data-files`. 204 | 205 | - Cabal issue [#6106](https://github.com/haskell/cabal/issues/6106) 206 | may require you to edit `--dependencies-only` build step, as explained above. 207 | 208 | - Git Hash into built executable. My approach is to ignore whole `.git` 209 | directory, as it might grow quite large. 210 | Maybe unignoring (with `!`) of `.git/HEAD` and `.git/refs` (which are relatively small) 211 | will make `gitrev` and alike work. Please tell me if you try! 212 | 213 | - Caching of Haskell dependencies is very rudimentary. 214 | It could be improved largely, if `/cabal/store` could be copied out 215 | after the build, and in before the build. I don't really know how to that 216 | in Docker. Any tips are welcome. 217 | For example with `docker run` one could use volumes, but not with `docker build`. 218 | 219 | Finally 220 | ------- 221 | 222 | Look at [the example repository](https://github.com/phadej/docker-haskell-example). 223 | I hope this is useful for you. 224 | --------------------------------------------------------------------------------