├── .dockerignore ├── .gitignore ├── README.md ├── config └── config.exs ├── docker ├── Dockerfile.build.alpine ├── Dockerfile.build.debian ├── Dockerfile.build.elixir ├── Dockerfile.build.elixir.faster ├── Dockerfile.build.fedora25 └── Dockerfile.build.phoenix ├── docs ├── distill_with_docker_pt1.md ├── distill_with_docker_pt2.md ├── distill_with_docker_pt3.md └── resources.md ├── lib ├── router.ex └── sample_plug_app.ex ├── mix.exs ├── mix.lock ├── rel └── config.exs └── test ├── sample_plug_app_test.exs └── test_helper.exs /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | # there are valid reasons to keep the .git, namely so that you can get the 3 | # current commit hash 4 | #.git 5 | .log 6 | tmp 7 | 8 | # we don't need our docker directory 9 | docker 10 | 11 | # Mix artifacts 12 | _build 13 | deps 14 | *.ez 15 | 16 | # don't include our release builds 17 | releases 18 | 19 | # Generate on crash by the VM 20 | erl_crash.dump 21 | 22 | # Static artifacts 23 | node_modules 24 | priv -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # npm installed files 20 | node_modules/ 21 | 22 | .deliver/releases/ 23 | releases/ 24 | *.tar.gz 25 | 26 | 27 | # Hugo 28 | pages/public/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploying Elixir 2 | 3 | I had some thoughts about deploying Elixir and decided to start putting them together. I like to start off small and see what grows. So there's a tiny sample plug app in lib, the content is in the docs directory, and the sample Dockerfiles are in docker. 4 | 5 | I do tend to be a bit opinionated but I'm constantly looking for better ideas, so if there's something that you don't like or think could be better, please open an issue and create a pull request. 6 | 7 | **Disclaimer:** this isn't intended to be a complete copy/paste deployment solution. It's a set of concepts and ideas that you can implement as needed. 8 | 9 | ### Overview 10 | 11 | Here's the overview of the process: 12 | 13 | - Build our app using [Distillery](https://github.com/bitwalker/distillery) to create our release 14 | - The build is going to happen inside a Docker container 15 | - Deploy to a Docker container or VPS/Bare Metal Host 16 | 17 | ### Things you need to know 18 | 19 | If you're coming from a language like Ruby or Node there are a few differences in deploying an Elixir application. Each of these can cause problems on their own but are easy to work around. 20 | 21 | 1. there's going to be a compile phase (and possibly asset building too), 22 | 2. that compile/build is going to output an Erlang deployable, and 23 | 3. the build environment (OS and version) must **match** the deployment/production environment (OS and version). 24 | 25 | I don't cover installing [Docker](https://docs.docker.com/engine/installation/) or setting up [Distillery](https://github.com/bitwalker/distillery), there's a lot of good documentation there already. 26 | 27 | [**Note:** Item #3 is probably going to cause some of you some heartburn because it's not absolutely 100% true. While there are some exceptions, treat it like it's completely true and your life will be much easier. The errors that happen because of mismatches can be rather obtuse and return very few Google hits. Keep things simple, match the build and deploy environments, once that's mastered if there's a requirement outside that scope evaluate it then.] 28 | 29 | There are many different ways to build and deploy an Elixir app, **many**. In fact, there's a whole page of [other resources](./docs/resources.md) with information on some of those ways. My biases are going to come out, so let me state a few right up front: 30 | 31 | - While I've taken an incremental approach here, the goal is to completely automate the whole thing with a CI process, 32 | - To get to a CI process you need to separate you dev environment from your build/test/production environments 33 | - Having spent a few years in the computer security world, I'm not a fan of installing build tools in production environments, so our process will be to install the build tools only in the build environment and deploy prebuilt binaries and assets to production 34 | - This means a separation of dev from build and build from production 35 | - Containers give us a direct way to match our build environment to our production one 36 | 37 | ## Distilling with Docker 38 | [Part 1: A Good Begining](./docs/distill_with_docker_pt1.md) 39 | [Part 2: Build it Faster](./docs/distill_with_docker_pt2.md) 40 | [Part 3: Comments and Q&A](./docs/distill_with_docker_pt3.md) 41 | 42 | 43 | ## Bottling a Release 44 | 45 | ## Run 46 | 47 | ## But what about... 48 | 49 | See the page of [resources](./docs/resources.md) 50 | 51 | ## Acknowledgements 52 | While writing this I came across [Elixir Docker Image Builder](https://github.com/edib-tool) and [Github - PagerDuty/docker_distiller](https://github.com/PagerDuty/docker_distiller). There's lots of overlapping concepts between what I've done and their build steps. Effectively, I've explained the process a bit and they've automated it. They are a bit opinionated and targeted at creating the smallest runtime docker image possible. 53 | 54 | [Releasing Elixir/OTP applications to the World](https://kennyballou.com/blog/2016/05/elixir-otp-releases/) Does an excellent job explaining the problem in detail. 55 | [Lessons from Building a Node App in Docker](http://jdlm.info/articles/2016/03/06/lessons-building-node-app-docker.html) 56 | [Github - Bitwalker's Phoenix Dockerfile](https://github.com/bitwalker/alpine-elixir-phoenix) -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :sample_plug_app, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:sample_plug_app, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /docker/Dockerfile.build.alpine: -------------------------------------------------------------------------------- 1 | # adapted from github.com/bitwalker/alpine-elixir 2 | # and github.com/bitwalker/alpine-erlang:latest 3 | 4 | # 2017-01-15 update to elixir 1.4.0 5 | 6 | FROM gliderlabs/alpine:3.4 7 | 8 | MAINTAINER Your Name 9 | 10 | # Important! Update this no-op ENV variable when this Dockerfile 11 | # is updated with the current date. It will force refresh of all 12 | # of the base images and things like `apt-get update` won't be using 13 | # old cached versions when the Dockerfile is built. 14 | ENV REFRESHED_AT=2017-01-15 \ 15 | LANG=en_US.UTF-8 \ 16 | HOME=/opt/app/ \ 17 | # Set this so that CTRL+G works properly 18 | TERM=xterm \ 19 | ERLANG_VER=19.1.6 \ 20 | ELIXIR_VER=1.4.0 21 | 22 | WORKDIR /tmp/erlang-build 23 | 24 | # Install updates and build tools (we're going to reuse these so make them a layer of their own) 25 | RUN \ 26 | # Create default user and home directory, set owner to default 27 | mkdir -p ${HOME} && \ 28 | adduser -s /bin/sh -u 1001 -G root -h ${HOME} -S -D default && \ 29 | chown -R 1001:0 ${HOME} && \ 30 | # Add edge repos tagged so that we can selectively install edge packages 31 | echo "@edge http://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories && \ 32 | # Upgrade Alpine and base packages 33 | apk --no-cache --update upgrade && \ 34 | # Install Erlang/OTP deps 35 | apk add --no-cache \ 36 | ca-certificates \ 37 | openssl-dev \ 38 | ncurses-dev \ 39 | unixodbc-dev \ 40 | zlib-dev \ 41 | git \ 42 | autoconf \ 43 | build-base \ 44 | make \ 45 | perl-dev && \ 46 | # Update ca certificates 47 | update-ca-certificates --fresh 48 | 49 | 50 | # Install Erlang 51 | RUN \ 52 | # Shallow clone Erlang/OTP 19.1.6 53 | git clone -b OTP-${ERLANG_VER} --single-branch --depth 1 https://github.com/erlang/otp.git . && \ 54 | # Erlang/OTP build env 55 | export ERL_TOP=/tmp/erlang-build && \ 56 | export PATH=$ERL_TOP/bin:$PATH && \ 57 | export CPPFlAGS="-D_BSD_SOURCE $CPPFLAGS" && \ 58 | # Configure 59 | ./otp_build autoconf && \ 60 | ./configure --prefix=/usr \ 61 | --sysconfdir=/etc \ 62 | --mandir=/usr/share/man \ 63 | --infodir=/usr/share/info \ 64 | --without-javac \ 65 | --without-wx \ 66 | --without-debugger \ 67 | --without-observer \ 68 | --without-jinterface \ 69 | --without-common_test \ 70 | --without-cosEvent\ 71 | --without-cosEventDomain \ 72 | --without-cosFileTransfer \ 73 | --without-cosNotification \ 74 | --without-cosProperty \ 75 | --without-cosTime \ 76 | --without-cosTransactions \ 77 | --without-dialyzer \ 78 | --without-et \ 79 | --without-gs \ 80 | --without-ic \ 81 | --without-megaco \ 82 | --without-orber \ 83 | --without-percept \ 84 | --without-typer \ 85 | --enable-threads \ 86 | --enable-shared-zlib \ 87 | --enable-ssl=dynamic-ssl-lib \ 88 | --enable-hipe && \ 89 | # Build 90 | make -j4 && make install 91 | 92 | # Move on to Elixir 93 | WORKDIR /tmp/elixir-build 94 | 95 | RUN \ 96 | git clone https://github.com/elixir-lang/elixir && \ 97 | cd elixir && \ 98 | git checkout v${ELIXIR_VER} && \ 99 | make && make install && \ 100 | mix local.hex --force && \ 101 | mix local.rebar --force 102 | 103 | WORKDIR ${HOME} 104 | 105 | CMD ["/bin/sh"] 106 | -------------------------------------------------------------------------------- /docker/Dockerfile.build.debian: -------------------------------------------------------------------------------- 1 | # Start with the "official" Elixir build (this simplifies quite a bit here). 2 | # This uses the "official" Erlang build (right now at 19.1.5 and includes rebar 2 & 3) 3 | # on top of Debian jessie. 4 | FROM elixir:1.4.0 5 | 6 | MAINTAINER Your Name 7 | 8 | ENV REFRESHED_AT 2017-01-15 9 | # 2017-01-15 update to elixir 1.4.0 10 | 11 | 12 | # Install hex 13 | RUN /usr/local/bin/mix local.hex --force && \ 14 | /usr/local/bin/mix hex.info 15 | 16 | # This prevents us from installing devDependencies 17 | # This causes brunch to build minified and hashed assets 18 | ENV MIX_ENV=prod BRUNCH_ENV=production 19 | 20 | WORKDIR /app/webapp 21 | # make sure you've added the .dockerignore file 22 | COPY . /app/webapp 23 | 24 | # since this is umbrella app we need the --all to get the dependencies for all apps 25 | # not just the umbrella app 26 | RUN mix deps.get && \ 27 | mix deps.compile && \ 28 | mix compile && \ 29 | mix release 30 | -------------------------------------------------------------------------------- /docker/Dockerfile.build.elixir: -------------------------------------------------------------------------------- 1 | # Start with the "official" Elixir build (this simplifies quite a bit here). 2 | # This uses the "official" Erlang build (right now at 19.2) 3 | # on top of Debian jessie. 4 | FROM elixir:1.4.0 5 | 6 | MAINTAINER Your Name 7 | 8 | ENV REFRESHED_AT 2017-01-15 9 | # 2017-01-15 update to elixir 1.4.0 10 | 11 | # Install hex 12 | RUN /usr/local/bin/mix local.hex --force && \ 13 | /usr/local/bin/mix local.rebar --force && \ 14 | /usr/local/bin/mix hex.info 15 | 16 | WORKDIR /app 17 | COPY . . 18 | 19 | RUN mix deps.get 20 | 21 | CMD ["bash"] 22 | -------------------------------------------------------------------------------- /docker/Dockerfile.build.elixir.faster: -------------------------------------------------------------------------------- 1 | # Start with the "official" Elixir build (this simplifies quite a bit here). 2 | # This uses the "official" Erlang build (right now at 19.2) 3 | # on top of Debian jessie. 4 | FROM elixir:1.4.0 5 | 6 | MAINTAINER Your Name 7 | 8 | ENV REFRESHED_AT 2017-01-15 9 | # 2017-01-15 update to elixir 1.4.0 10 | 11 | # Install hex 12 | RUN /usr/local/bin/mix local.hex --force && \ 13 | /usr/local/bin/mix local.rebar --force && \ 14 | /usr/local/bin/mix hex.info 15 | 16 | WORKDIR /app 17 | 18 | # Copy in only our mix files, the run will only happen if they've changed 19 | COPY mix.exs mix.lock ./ 20 | RUN mix do deps.get, deps.compile 21 | 22 | # Now copy in the rest of the app 23 | COPY . . 24 | 25 | CMD ["bash"] 26 | -------------------------------------------------------------------------------- /docker/Dockerfile.build.fedora25: -------------------------------------------------------------------------------- 1 | # This image is designed as a base image to use in creating distillery releases. It 2 | # includes quite a few things that you would not need for just a pure runtime. 3 | FROM fedora:25 4 | # Fedora 25 is going to give us Erlang 19 and Node 6.9 5 | # Elixir is at 1.3.1 and we want latest 1.3.4 so we will not install it from dnf 6 | 7 | MAINTAINER Your Name 8 | 9 | ENV REFRESHED_AT 2017-01-15 10 | # 2017-01-15 update to elixir 1.4.0 11 | # 2016-12-14 clean up comments and any remaining debainisms 12 | # 2016-11-30 Refactor for Fedora 25 13 | # 2016-11-02 updated to erlang 19 and elixir 1.3 14 | # 2015-12-20 update erlang to 18.* so that it will pick up the latest one (18.2 isn't in repo yet) 15 | 16 | ENV DEBIAN_FRONTEND noninteractive 17 | ENV LANG en_US.UTF-8 18 | ENV LANGUAGE en_US:en 19 | ENV LC_ALL en_US.UTF-8 20 | 21 | # Get base packages, dev tools, and set the locale 22 | RUN dnf -y update --setopt=deltarpm=false && \ 23 | dnf -y install --setopt=deltarpm=false \ 24 | @development-tools \ 25 | curl \ 26 | erlang \ 27 | git \ 28 | nodejs \ 29 | unzip \ 30 | wget \ 31 | && dnf clean all 32 | 33 | # Download and Install Specific Version of Elixir 34 | WORKDIR /elixir 35 | RUN wget -q https://github.com/elixir-lang/elixir/releases/download/v1.4.0/Precompiled.zip && \ 36 | unzip Precompiled.zip && \ 37 | rm -f Precompiled.zip && \ 38 | ln -s /elixir/bin/elixirc /usr/local/bin/elixirc && \ 39 | ln -s /elixir/bin/elixir /usr/local/bin/elixir && \ 40 | ln -s /elixir/bin/mix /usr/local/bin/mix && \ 41 | ln -s /elixir/bin/iex /usr/local/bin/iex 42 | 43 | # Install local Elixir hex and rebar 44 | RUN /usr/local/bin/mix local.hex --force && \ 45 | /usr/local/bin/mix local.rebar --force && \ 46 | /usr/local/bin/mix hex.info 47 | 48 | ENV PORT 4000 49 | ENV MIX_ENV prod 50 | # This prevents us from installing devDependencies 51 | # ENV NODE_ENV production 52 | # This causes brunch to build minified and hashed assets 53 | ENV BRUNCH_ENV production 54 | 55 | WORKDIR /home/app/webapp 56 | COPY . /home/app/webapp 57 | 58 | # the phoenix.js and phoenix_html dependencies in package json require 59 | # us to copy the whole web app in first and get dependencies before 60 | # doing the npm install 61 | 62 | # since this is umbrella app we need the --all to get the dependencies for all apps 63 | # not just the umbrella app 64 | RUN mix deps.get --all && \ 65 | mix deps.compile --all && \ 66 | mix compile --all && \ 67 | mix release --env=prod 68 | 69 | # go to our phoenix app and install node dependencies and output static assets 70 | # do this after mix deps.get since the phoenix & phoenix_html node modules reference 71 | # files in these dependencies 72 | # WORKDIR /home/app/webapp/apps/search_web 73 | # RUN npm install \ 74 | # # && npm rebuild node-sass \ 75 | # && node node_modules/brunch/bin/brunch build \ 76 | # && mix phoenix.digest 77 | 78 | # build the app 79 | # WORKDIR /home/app/webapp 80 | # RUN mix release --env=prod 81 | 82 | # EXPOSE 4000 83 | # CMD ["mix","phoenix.server"] 84 | RUN pwd 85 | RUN ls -la rel/tuscon/releases/0.1.0 -------------------------------------------------------------------------------- /docker/Dockerfile.build.phoenix: -------------------------------------------------------------------------------- 1 | # Start with the "official" Elixir build (this simplifies quite a bit here). 2 | # This uses the "official" Erlang build (right now at 19.1.5 and includes rebar 2 & 3) 3 | # on top of Debian jessie. 4 | FROM elixir:1.4.0 5 | 6 | MAINTAINER Paul Lamb 7 | 8 | ENV REFRESHED_AT 2017-01-15 9 | # 2017-01-15 update to elixir 1.4.0 10 | # 2017-01-01 Switch to copying from "official" node 11 | # 2016-11-03 Switch to "official" Elixir Dockerfile for base and node 6.7.0 12 | # 2016-11-02 updated to Erlang 19 and Elixir 1.3 13 | # 2015-12-20 update Erlang to 18.* so that it will pick up the latest one (18.2 isn't in repo yet) 14 | 15 | # Install hex 16 | RUN /usr/local/bin/mix local.hex --force && \ 17 | /usr/local/bin/mix hex.info 18 | 19 | 20 | # ----------------------------------------------------------------------------- 21 | # node:6.9.2 22 | # https://hub.docker.com/_/node/ 23 | RUN apt-get update && apt-get install -y --no-install-recommends \ 24 | autoconf \ 25 | automake \ 26 | bzip2 \ 27 | file \ 28 | g++ \ 29 | gcc \ 30 | imagemagick \ 31 | libbz2-dev \ 32 | libc6-dev \ 33 | libcurl4-openssl-dev \ 34 | libdb-dev \ 35 | libevent-dev \ 36 | libffi-dev \ 37 | libgdbm-dev \ 38 | libgeoip-dev \ 39 | libglib2.0-dev \ 40 | libjpeg-dev \ 41 | libkrb5-dev \ 42 | liblzma-dev \ 43 | libmagickcore-dev \ 44 | libmagickwand-dev \ 45 | libmysqlclient-dev \ 46 | libncurses-dev \ 47 | libpng-dev \ 48 | libpq-dev \ 49 | libreadline-dev \ 50 | libsqlite3-dev \ 51 | libssl-dev \ 52 | libtool \ 53 | libwebp-dev \ 54 | libxml2-dev \ 55 | libxslt-dev \ 56 | libyaml-dev \ 57 | make \ 58 | patch \ 59 | xz-utils \ 60 | zlib1g-dev \ 61 | && rm -rf /var/lib/apt/lists/* 62 | 63 | RUN groupadd --gid 1000 node \ 64 | && useradd --uid 1000 --gid node --shell /bin/bash --create-home node 65 | 66 | # gpg keys listed at https://github.com/nodejs/node 67 | RUN set -ex \ 68 | && for key in \ 69 | 9554F04D7259F04124DE6B476D5A82AC7E37093B \ 70 | 94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \ 71 | 0034A06D9D9B0064CE8ADF6BF1747F4AD2306D93 \ 72 | FD3A5288F042B6850C66B31F09FE44734EB7990E \ 73 | 71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \ 74 | DD8F2338BAE7501E3DD5AC78C273792F7D83545D \ 75 | B9AE9905FFD7803F25714661B63B535A4C206CA9 \ 76 | C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \ 77 | ; do \ 78 | gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key"; \ 79 | done 80 | 81 | ENV NPM_CONFIG_LOGLEVEL info 82 | ENV NODE_VERSION 6.9.2 83 | 84 | RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \ 85 | && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ 86 | && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \ 87 | && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \ 88 | && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \ 89 | && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \ 90 | && ln -s /usr/local/bin/node /usr/local/bin/nodejs 91 | # ----------------------------------------------------------------------------- 92 | 93 | 94 | # This prevents us from installing devDependencies 95 | # This causes brunch to build minified and hashed assets 96 | ENV MIX_ENV=prod BRUNCH_ENV=production 97 | 98 | WORKDIR /app 99 | COPY . /app 100 | 101 | RUN mix deps.get && \ 102 | mix deps.compile && \ 103 | mix compile 104 | 105 | # install node dependencies and output static assets 106 | # do this after mix deps.get since the phoenix & phoenix_html node modules reference 107 | # files in these dependencies 108 | RUN npm install \ 109 | && npm rebuild node-sass \ 110 | && node node_modules/brunch/bin/brunch build \ 111 | && mix phoenix.digest 112 | 113 | CMD ["bash"] -------------------------------------------------------------------------------- /docs/distill_with_docker_pt1.md: -------------------------------------------------------------------------------- 1 | # Distilling with Docker Part 1: A Good Begining 2 | 3 | - Last updated: January 2017 4 | - Elixir 1.4.0 5 | - Distillery 1.1.0 6 | - Docker 1.12 7 | 8 | ## Why 9 | 10 | There are a few reasons why you'd want to containerize your build step: 11 | 12 | 1. You tried to deploy a build from your dev environment (macOs) to a Linux machine and it didn't work, 13 | 2. You realized that you had NIFs (natively compiled code) in your dependencies (comeonin) 14 | 3. You had an Erlang dependency mismatch on deploy 15 | 4. You want to automate your build with a CI process 16 | 17 | Don't feel bad, I wrote this article because I hit all those problems. But once you've containerized the build you can rein in all those problems and the CI automation step becomes much easier. We're going to create a Dockerfile that builds a Distillery release of our app and saves it to disk, it's not complicated but there are a couple of things you need to know. 18 | 19 | ## But First 20 | You need to have both [Docker](https://docs.docker.com/engine/installation/) installed and [Distillery](https://github.com/bitwalker/distillery) setup and working for your app. 21 | 22 | ## Sample App & Dockerfiles 23 | I've created a very simple plug application, named SamplePlugApp, that I'll use to demonstrate the build process. If you'd like to follow along the github repo is at: (https://github.com/plamb/elixir-deploy)[https://github.com/plamb/elixir-deploy] 24 | 25 | ## Deploying to? 26 | 27 | I'm going to make the assumption that you are deploying to some form of Linux and for this first part we're going to target Debian Jessie. In part 2 we'll cover other targets but we need to start with something easy and get it working first, then we can iterate from there. Debian Jessie just happens to be what the ["official" Erlang docker image](https://hub.docker.com/_/erlang/) and ["official" Elixir docker image](https://hub.docker.com/_/elixir/) is built on, so that's what we're going to use. 28 | 29 | Keep in mind that your docker build environment needs to **match** your deployment environment. 30 | 31 | ## .dockerignore 32 | 33 | Everybody goes straight for the Dockerfile first. Don't. Create a .dockerignore file at the root of your project. This file tells Docker what to skip when we issue a COPY command in our Dockerfile. It may sound a bit trivial but on even a medium size apps it can offer a decent performance boost. Plus, and this is a big one, we DO NOT want to copy in our dev _build or deps directories into the Docker container. The whole point of this exercise is to completely separate out our dev build from the deploy build. We're wanting to exclude everything we possibly can and no more. Here's a sample one: 34 | 35 | ``` 36 | .dockerignore # check and see if this is excluded by default 37 | .git 38 | .gitignore 39 | .log 40 | tmp 41 | 42 | # I usually have a docker directory with Dockerfiles and scripts 43 | docker 44 | 45 | # Mix artifacts 46 | _build 47 | deps 48 | *.ez 49 | 50 | # we're going to store our releases in the releases directory, we don't need to copy these files into the container 51 | releases 52 | 53 | # Generate on crash by the VM 54 | erl_crash.dump 55 | 56 | # Static artifacts if you're using Phoenix if it's an umbrella app change the paths 57 | node_modules 58 | priv 59 | 60 | # any other directories that have files that don't need to be included in the build 61 | # these are specific to the sample project 62 | docs 63 | ``` 64 | 65 | ## Distillery Config 66 | 67 | By default, Distillery is going to save releases to '_build/<$MIX_ENV>/rel/' but we need to change that because we're going to mount our releases directory into the Docker build container and have Distillery send output there. 68 | 69 | You need to modify rel/config.exs. [If you don't have a rel/config.exs file, you haven't set up [Distillery](https://github.com/bitwalker/distillery) yet.] Add an output_dir to the release block [it's usually at the bottom] and change the app name to match yours. 70 | 71 | ```elixir 72 | release :sample_plug_app do 73 | set version: current_version(:sample_plug_app) 74 | set output_dir: './releases/sample_plug_app' 75 | end 76 | ``` 77 | Note: the output_dir should be included in the .dockerignore file. If you change to a different directory make sure to also update the .dockerignore. Also, make sure you change the "sample_plug_app" to the name of your app. 78 | 79 | ## Releases directory 80 | 81 | Create the releases directory and update the permissions so that the docker daemon can write to it. [Note: I haven't researched this in depth and went the easy way with permissions.] 82 | 83 | ```bash 84 | mkdir releases 85 | chmod 0777 releases 86 | ``` 87 | 88 | 89 | ## Dockerfile 90 | 91 | With our .dockerignore and updated Distillery config, we're finally to our Dockerfile. I create a `docker` directory and put docker relates files in there. The one below is [docker/Dockerfile.build.elixir](./docker/Dockerfile.build.elixir). It uses the "official" Elixir layer which includes the "official" Erlang layer which is based on Debian Jessie. 92 | 93 | ```dockerfile 94 | FROM elixir:1.4.0 95 | 96 | MAINTAINER Your Name 97 | 98 | ENV REFRESHED_AT 2017-01-15 99 | # 2017-01-15 update to elixir 1.4.0 100 | 101 | # Install hex 102 | RUN /usr/local/bin/mix local.hex --force && \ 103 | /usr/local/bin/mix local.rebar --force && \ 104 | /usr/local/bin/mix hex.info 105 | 106 | WORKDIR /app 107 | COPY . . 108 | 109 | RUN mix deps.get 110 | 111 | CMD ["bash"] 112 | ``` 113 | 114 | You can then issue the following command to build a release. If you've been testing Distillery and have output in the releases directory, please do a `mix release.clean` before the `docker build` and `docker run`. 115 | 116 | ``` 117 | docker build --tag=build-elixir -f docker/Dockerfile.build.elixir . 118 | docker run -v $PWD/releases:/app/releases build-elixir mix release --env=prod 119 | ``` 120 | 121 | What does this do? First, we build a Docker image using the Dockerfile we created. The build utilizes the existing "official" Elixir layer, adds hex, copies our application into the container and gets the dependencies. We tag the container with a name of "build-release". The first time you run the build step it's is going to take a while, subsequent builds will use Docker's caching mechanisms and will go much faster. In fact, subsequent builds should only execute the COPY and RUN to get code and dependencies. 122 | 123 | The second command, `docker run`, will execute the command `mix release --env=prod` within the container we just created, which will compile and package our app. Our release tarball will be stored in releases/sample_plug_app/releases/0.1.0/sample_plug_app.tar.gz. 124 | 125 | 126 | # See Also 127 | [Distilling with Docker, Part 1: A Good Begining](./distill_with_docker_pt1.md) 128 | 129 | [Distilling with Docker, Part 2: Build it Faster](./distill_with_docker_pt2.md) 130 | 131 | [Distilling with Docker, Part 3: Comments and Q&A](./distill_with_docker_pt3.md) 132 | 133 | 134 | -------------------------------------------------------------------------------- /docs/distill_with_docker_pt2.md: -------------------------------------------------------------------------------- 1 | # Distilling with Docker Part 2: Build it Faster 2 | 3 | - Last updated: January 2017 4 | - Elixir 1.4.0 5 | - Distillery 1.1.0 6 | - Docker 1.12 7 | 8 | In [Part 1](./docs/distill_with_docker_pt1.md) we setup a basic build process for creating Elixir releases with [Distillery](https://github.com/bitwalker/distillery). Now that we've got it working, let's make it faster. 9 | 10 | ##Caching Layers 11 | We're going to make a change to our Dockerfile and add the mix.exs and mix.lock file in it's own layer. 12 | 13 | ```dockerfile 14 | FROM elixir:1.4.0 15 | 16 | MAINTAINER Your Name 17 | 18 | ENV REFRESHED_AT 2017-01-15 19 | 20 | # Install hex 21 | RUN /usr/local/bin/mix local.hex --force && \ 22 | /usr/local/bin/mix local.rebar --force && \ 23 | /usr/local/bin/mix hex.info 24 | 25 | WORKDIR /app 26 | 27 | # Copy in only our mix files, the run will only happen if they've changed 28 | COPY mix.exs mix.lock ./ 29 | RUN mix do deps.get, deps.compile 30 | 31 | # Now copy in the rest of the app 32 | COPY . . 33 | 34 | CMD ["bash"] 35 | ``` 36 | 37 | Since each docker step is dependent on the previous one, we use the inherent caching in Docker to only execute a step if something has change. Thus the `RUN mix do deps.get, deps.compile` only gets executed if the files in preceding COPY command have changed. 38 | 39 | # See Also 40 | [Distilling with Docker, Part 1: A Good Begining](./distill_with_docker_pt1.md) 41 | 42 | [Distilling with Docker, Part 2: Build it Faster](./distill_with_docker_pt2.md) 43 | 44 | [Distilling with Docker, Part 3: Comments and Q&A](./distill_with_docker_pt3.md) 45 | -------------------------------------------------------------------------------- /docs/distill_with_docker_pt3.md: -------------------------------------------------------------------------------- 1 | # Distilling with Docker Part 3: Comments and Q&A 2 | 3 | - Last updated: January 2017 4 | - Elixir 1.4.0 5 | - Distillery 1.1.0 6 | - Docker 1.12 7 | 8 | ## Q & A 9 | 10 | There's going to be some question and I'll try and answer a few right up front. 11 | 12 | #### But...what about? 13 | Yes, there's a couple of other ways we could have done the Dockerfile and the commands. Particularly, why didn't we just use another volume and map our source directory right into the container? Mostly, because I really, really want a separation between our dev, build, test and prod environments. I did not want any local dev bits getting into our build. 14 | 15 | #### Why can't you do it all with the docker build command? 16 | You can only mount volumes when running the `docker run` command. To get around this, we could have done a `RUN mix release` in the Dockerfile and then used a `docker cp` to copy the file out. But this means that the commands run on the container are hard coded into the container. Instead, we use a `CMD [bash]` to tell the container what to execute when a `docker run` is issued without any arguments, we then override that command with the final option `mix release --env=prod`. This gives us an easy way to specify options and commands to the container, i.e. `mix release.upgrade --env=prod`. 17 | 18 | #### But I want to git clone the source into the container. 19 | At some point, I might write that how-to, maybe. But I'd suggest you let that automagically happen with your CI system instead. In the meantime, you might take a look at [Building docker images with two Dockerfiles](http://blog.tomecek.net/post/build-docker-image-in-two-steps). 20 | 21 | #### What about a Phoenix app? 22 | Take a look at the highly untested [docker/Dockerfile.build.phoenix](./docker/Dockerfile.build.phoenix). There's a few more steps, including adding Nodejs for asset compilation. 23 | 24 | #### What about environment variables? 25 | Ugh...yeah...this can trip you up. An article just on this is "coming soon". I promise. 26 | 27 | # See Also 28 | [Distilling with Docker, Part 1: A Good Begining](./distill_with_docker_pt1.md) 29 | 30 | [Distilling with Docker, Part 2: Build it Faster](./distill_with_docker_pt2.md) 31 | 32 | [Distilling with Docker, Part 3: Comments and Q&A](./distill_with_docker_pt3.md) 33 | -------------------------------------------------------------------------------- /docs/resources.md: -------------------------------------------------------------------------------- 1 | # Build/Deploy Resources 2 | 3 | ## Heroku 4 | [Hashnuke's Elixir Buildpack](https://github.com/HashNuke/heroku-buildpack-elixir) 5 | [GJaldon's Phoenix Buildpack](https://github.com/gjaldon/heroku-buildpack-phoenix-static) 6 | 7 | ## Edeliver 8 | [Github - Edeliver](https://github.com/boldpoker/edeliver) 9 | [Deploying Elixir applications with Edeliver](http://blog.plataformatec.com.br/2016/06/deploying-elixir-applications-with-edeliver/) 10 | [How We Deploy Elixir Apps](http://big-elephants.com/2016-06/how-we-deploy-elixir-apps/) 11 | [Deploying an Elixir Umbrella project using Distillery and Edeliver](https://medium.com/@brucepomeroy/deploying-an-elixir-umbrella-project-using-distillery-and-edeliver-b0e8528569e3#.32kxb41su) 12 | 13 | ## Gattling 14 | [Github - Gattling](https://github.com/hashrocket/gatling) 15 | [How I Built My Own Heroku for Phoenix Apps: Part 1](https://hashrocket.com/blog/posts/how-i-built-my-own-heroku-for-phoenix-apps-part-1) 16 | [How I Built My Own Heroku for Phoenix Apps: Part 2](https://hashrocket.com/blog/posts/how-i-built-my-own-heroku-for-phoenix-apps-part-2) 17 | [Deploying Phoenix on Ubuntu with Gatling](https://dennisreimann.de/articles/phoenix-deployment-gatling-ubuntu-digital-ocean.html) 18 | 19 | ## Chef/Puppet/Ansible 20 | [Continuous Delivery for Elixir (Part 1 — Introduction)](https://medium.com/@jeffweiss/continuous-delivery-with-elixir-part-1-introduction-826ae5ddb569#.4k3t62m0b) 21 | [Continuous Delivery for Elixir (Part 2 — Our Project)](https://medium.com/@jeffweiss/continuous-delivery-for-elixir-part-2-our-project-ae6406c6a990#.iemlr55ub) 22 | [Continuous Delivery for Elixir (Part 3 — conform is the new norm)](https://medium.com/@jeffweiss/continuous-delivery-for-elixir-part-3-conform-is-the-new-norm-f6a2e8552bff#.munzv8v0u) 23 | [Continuous Delivery for Elixir (Part 4 — Use a release, troubles cease)](https://medium.com/@jeffweiss/continuous-delivery-for-elixir-part-4-use-a-release-troubles-cease-15af765b4772#.mloo6lc5a) 24 | [Continuous Delivery for Elixir (Part 5 — Use a package to get trackage)](https://medium.com/@jeffweiss/continuous-delivery-for-elixir-part-5-use-a-package-to-get-trackage-6b1bc3545824#.ngjfospik) 25 | 26 | ## Kubernets 27 | [Bitwalker's Swarm](https://github.com/bitwalker/swarm) 28 | [Clustering Elixir nodes on Kubernetes](https://substance.brpx.com/clustering-elixir-nodes-on-kubernetes-e85d0c26b0cf#.9lh03owmg) 29 | 30 | ## Blog Posts 31 | [SemaphoreCI: Dockerizing Elixir and Phoenix Applications](https://semaphoreci.com/community/tutorials/dockerizing-elixir-and-phoenix-applications) 32 | [Creating a good, secure Docker base image](http://heiber.im/post/creating-a-solid-docker-base-image/) 33 | [CodeShip: Reducing Your Docker Image Size](https://blog.codeship.com/reduce-docker-image-size/) 34 | [Using Flynn to deploy a Phoenix app](http://nsomar.com/how-to-use-flynn-to-deploy-a-phenix-app/) -------------------------------------------------------------------------------- /lib/router.ex: -------------------------------------------------------------------------------- 1 | defmodule SamplePlugApp.Router do 2 | use Plug.Router 3 | 4 | plug :match 5 | plug :dispatch 6 | 7 | get "/hello" do 8 | send_resp(conn, 200, "world") 9 | end 10 | 11 | 12 | get "/healthz" do 13 | send_resp(conn, 200, "OK") 14 | end 15 | 16 | 17 | match _ do 18 | send_resp(conn, 404, "oops") 19 | end 20 | end -------------------------------------------------------------------------------- /lib/sample_plug_app.ex: -------------------------------------------------------------------------------- 1 | defmodule SamplePlugApp do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | # Define workers and child supervisors to be supervised 12 | Plug.Adapters.Cowboy.child_spec(:http, Tuscon.Router, [], [port: 4001]) 13 | ] 14 | 15 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 16 | # for other strategies and supported options 17 | opts = [strategy: :one_for_one, name: Tuscon.Supervisor] 18 | Supervisor.start_link(children, opts) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule SamplePlugApp.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :sample_plug_app, 6 | version: "0.1.0", 7 | elixir: "~> 1.4", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps()] 11 | end 12 | 13 | def application do 14 | [applications: [:logger, :cowboy, :plug], 15 | mod: {SamplePlugApp, []}] 16 | end 17 | 18 | defp deps do 19 | [ 20 | {:cowboy, "~> 1.0"}, 21 | {:plug, "~> 1.3"}, 22 | {:distillery, "~> 1.1.0"} 23 | ] 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:make, :rebar], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]}, 2 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, 3 | "distillery": {:hex, :distillery, "1.1.0", "e9943bd29557e9c252a051d8ac4b47e597cd9bf2a74332b8628eab4954eb51d7", [:mix], []}, 4 | "mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], []}, 5 | "plug": {:hex, :plug, "1.3.0", "6e2b01afc5db3fd011ca4a16efd9cb424528c157c30a44a0186bcc92c7b2e8f3", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, 6 | "ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []}} 7 | -------------------------------------------------------------------------------- /rel/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Releases.Config, 2 | # This sets the default release built by `mix release` 3 | default_release: :default, 4 | # This sets the default environment used by `mix release` 5 | default_environment: :dev 6 | 7 | # For a full list of config options for both releases 8 | # and environments, visit https://hexdocs.pm/distillery/configuration.html 9 | 10 | 11 | # You may define one or more environments in this file, 12 | # an environment's settings will override those of a release 13 | # when building in that environment, this combination of release 14 | # and environment configuration is called a profile 15 | 16 | environment :dev do 17 | set dev_mode: true 18 | set include_erts: false 19 | set cookie: :"=]}r5pQUHPyd8[@5v/:c$l&cM(j(n^.YE)b:Rww:2z.eo$9" 20 | end 21 | 22 | environment :prod do 23 | set include_erts: true 24 | set include_src: false 25 | set cookie: :"4$UGcH]SECQ~JjZC;z1zP9FI>idEww.$M3:?C;k_~YHK._ej?:Phcx78_[F^(&b~" 26 | end 27 | 28 | # You may define one or more releases in this file. 29 | # If you have not set a default release, or selected one 30 | # when running `mix release`, the first release in the file 31 | # will be used by default 32 | 33 | release :sample_plug_app do 34 | set version: current_version(:sample_plug_app) 35 | # set output_dir: './releases/sample_plug_app' 36 | end 37 | 38 | -------------------------------------------------------------------------------- /test/sample_plug_app_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SamplePlugAppTest do 2 | use ExUnit.Case 3 | doctest SamplePlugApp 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------