├── mix.lock ├── .gitignore ├── priv └── templates │ ├── release.Dockerfile.eex │ └── build.Dockerfile.eex ├── mix.exs ├── config └── config.exs ├── lib ├── dockerator.ex └── mix │ └── tasks │ └── dockerate.ex └── README.md /mix.lock: -------------------------------------------------------------------------------- 1 | %{"distillery": {:hex, :distillery, "1.5.2", "eec18b2d37b55b0bcb670cf2bcf64228ed38ce8b046bb30a9b636a6f5a4c0080", [:mix], []}, 2 | "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [], [], "hexpm"}, 3 | "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}} 4 | -------------------------------------------------------------------------------- /.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 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /priv/templates/release.Dockerfile.eex: -------------------------------------------------------------------------------- 1 | FROM <%= base_image %> 2 | ENV DEBIAN_FRONTEND=noninteractive MIX_ENV=<%= mix_env %> 3 | <%= for release_extra_docker_command <- release_extra_docker_commands do %> 4 | <%= release_extra_docker_command %> 5 | <% end %> 6 | COPY <%= build_output_path_relative %>/app/ /opt/app/ 7 | <%= unless run_as_root do %> 8 | RUN groupadd -g 999 app && useradd -r -u 999 -g app app 9 | RUN mkdir /opt/app/var && chown app:app /opt/app/var 10 | USER app 11 | <% end %> 12 | HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 \ 13 | CMD ["/opt/app/bin/<%= rel_name %>", "ping"] 14 | ENTRYPOINT ["/opt/app/bin/<%= rel_name %>", "foreground"] -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Dockerator.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :dockerator, 7 | version: "1.3.3", 8 | elixir: "~> 1.4", 9 | start_permanent: Mix.env == :stag or Mix.env == :prod, 10 | deps: deps(), 11 | description: description(), 12 | package: package(), 13 | ] 14 | end 15 | 16 | def application do 17 | [ 18 | extra_applications: [:logger] 19 | ] 20 | end 21 | 22 | defp deps do 23 | [ 24 | {:distillery, "~> 1.5", runtime: false}, 25 | {:ex_doc, ">= 0.0.0", only: :dev}, 26 | ] 27 | end 28 | 29 | defp description do 30 | """ 31 | Tool for turning Elixir apps into Docker images without a pain. 32 | """ 33 | end 34 | 35 | defp package do 36 | [ 37 | files: ["lib", "priv", "mix.exs", "README*"], 38 | maintainers: ["Marcin Lewandowski"], 39 | licenses: ["MIT"], 40 | links: %{"GitHub" => "https://github.com/dockerator/dockerator-elixir"}, 41 | ] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /priv/templates/build.Dockerfile.eex: -------------------------------------------------------------------------------- 1 | FROM <%= base_image %> 2 | ENV DEBIAN_FRONTEND=noninteractive MIX_ENV=<%= mix_env %> 3 | 4 | <%= for build_extra_docker_command <- build_extra_docker_commands do %> 5 | <%= build_extra_docker_command %> 6 | <% end %> 7 | 8 | RUN mkdir /root/src 9 | <%= for dir <- source_dirs do %> 10 | <%= if File.exists?(dir) and File.dir?(dir) do %> 11 | COPY <%= dir %>/ /root/src/<%= dir %>/ 12 | <% end %> 13 | <% end %> 14 | COPY mix.exs /root/src/ 15 | COPY mix.lock /root/src/ 16 | 17 | RUN \ 18 | <%= unless Enum.empty?(git_deps_urls) do %> 19 | command -v git || (apt-get update && apt-get install git -y) && \ 20 | mkdir -p /root/.ssh && \ 21 | chmod 700 /root/.ssh && \ 22 | 23 | <%= for git_dep_url <- git_deps_urls do %> 24 | <%= case URI.parse(git_dep_url) do %> 25 | <% %URI{scheme: "ssh", host: host, port: nil} -> %> 26 | ssh-keyscan <%= host %> >> /root/.ssh/known_hosts && \ 27 | <% %URI{scheme: "ssh", host: host, port: port} -> %> 28 | ssh-keyscan -p <%= port %> <%= host %> >> /root/.ssh/known_hosts && \ 29 | <% _ -> %> 30 | <% end %> 31 | <% end %> 32 | 33 | chmod 600 /root/.ssh/known_hosts && \ 34 | <% end %> 35 | mix local.hex --force && \ 36 | mix local.rebar --force 37 | 38 | WORKDIR /root/src 39 | -------------------------------------------------------------------------------- /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 your application as: 12 | # 13 | # config :dockerator, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:dockerator, :key) 18 | # 19 | # You can also 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 | -------------------------------------------------------------------------------- /lib/dockerator.ex: -------------------------------------------------------------------------------- 1 | defmodule Dockerator do 2 | @moduledoc """ 3 | Tool for turning Elixir apps into Docker images without a pain. 4 | 5 | ## Rationale 6 | 7 | One may say that creating a Dockerfile for an Elixir app is so easy that 8 | creating a separate tool for such purpose is an overkill. 9 | 10 | However, that might be not that easy if: 11 | 12 | * You need to maintain a lot of apps and you want to ensure thay use the same 13 | build system without manually replicating tons of Dockerfiles. 14 | * You use dependencies stored on private git repositories. 15 | 16 | In such cases Dockerator will save you a lot of time. 17 | 18 | ## Features 19 | 20 | * **Clean build environment** - It always builds the release of Elixir project 21 | in the clean environment in order to ensure repeatable builds. 22 | * **No source code in the image** - The target image will not contain the 23 | source code, just the compiled release. 24 | * **SSH agent forwarding** - It can handle SSH agent forwarding so you can use 25 | dependencies stored at private SSH repositories without exposing your 26 | credentials. 27 | 28 | Internally it uses [Distillery](https://github.com/bitwalker/distillery) for 29 | building the actual release. 30 | 31 | 32 | # Prerequisities 33 | 34 | You need to use Elixir >= 1.4. 35 | 36 | You need to have on your computer a [Docker](https://docker.io) installation. 37 | The `docker` command should be callable without `sudo`. 38 | 39 | 40 | # Usage 41 | 42 | Add it to the dependencies, by adding the following the `deps` in `mix.exs`: 43 | 44 | ```elixir 45 | def deps do 46 | [ 47 | {:dockerator, "~> 1.3", runtime: false}, 48 | ] 49 | end 50 | ``` 51 | 52 | Moreover add the key `:dockerator_target_image` to the `app` with name of the 53 | target Docker image. 54 | 55 | Then fetch the dependencies: 56 | 57 | ```bash 58 | mix deps.get 59 | ``` 60 | 61 | Create release configuration (if it is not present yet): 62 | 63 | ```bash 64 | mix release.init 65 | ``` 66 | 67 | It will create `rel/` directory, add it to git: 68 | 69 | ```bash 70 | git add rel/ 71 | ``` 72 | 73 | 74 | Then you can just call the following command each time you need to assemble 75 | a Docker image tagged as `latest`: 76 | 77 | ```bash 78 | mix dockerate 79 | ``` 80 | 81 | If you want to make the actual release, please increase version in the 82 | `mix.exs` (potentially you want to also tag the code in git) and then run 83 | 84 | ```bash 85 | mix dockerate release 86 | ``` 87 | 88 | The Docker image will use version from `mix.exs` as a tag. 89 | 90 | You probably want to also change the Mix environment, just prefix the 91 | commands with MIX_ENV=env, e.g.: 92 | 93 | 94 | ```bash 95 | MIX_ENV=prod mix dockerate release 96 | ``` 97 | 98 | 99 | 100 | # Configuration 101 | 102 | You can use the following settings in the `project` of the `mix.exs` in order 103 | to configure Dockerator: 104 | 105 | * `:dockerator_target_image` - (mandatory) - a string containing target 106 | Docker image name, e.g. `"myaccount/my_app"`. 107 | * `:dockerator_base_image` - (optional) - a string or keyword list containing 108 | name of a base Docker image name used for build and release. If it is 109 | a string, it will use provided name for both build and release. If it is 110 | a keyword list, you can specify two keys `:build` or/and `:release` to 111 | specify different images for these two phases. Defaults to 112 | `elixir:latest`. It is strongly encouraged to change this to the particular 113 | [Elixir version](https://hub.docker.com/r/library/elixir/tags/) to have 114 | repeatable builds. 115 | * `:dockerator_ssh_agent` - (optional) - a boolean indicating whether 116 | we should use SSH agent for the build. Defaults to `false`. Turn it on 117 | if you're using dependencies that are hosted on private git/SSH repositories. 118 | * `:dockerator_source_dirs` - (optional) - a list of strings containing a list 119 | of source directories that will be copied to the build image. Defaults to 120 | `["config", "lib", "rel", "priv", "web"]`. 121 | * `:dockerator_build_extra_docker_commands` - optional - a list of strings that 122 | will contain extra commands that will be added to the release image. For 123 | example you can add something like `["apt-get install something"]`. 124 | * `:dockerator_release_extra_docker_commands` - optional - a list of strings that 125 | will contain extra commands that will be added to the release image. For 126 | example you can add something like `["EXPOSE 4000"]`. 127 | 128 | ## Example 129 | 130 | For example your `mix.exs` might look like this after the changes: 131 | 132 | ```elixir 133 | defmodule MyApp.Mixfile do 134 | use Mix.Project 135 | 136 | def project do 137 | [app: :my_app, 138 | version: "0.1.0", 139 | elixir: "~> 1.4", 140 | build_embedded: Mix.env == :prod, 141 | start_permanent: Mix.env == :prod, 142 | deps: deps(), 143 | dockerator_ssh_agent: true, 144 | dockerator_build_extra_docker_commands: [ 145 | "RUN apt-get update && apt-get install somepackage", 146 | ], 147 | dockerator_release_extra_docker_commands: [ 148 | "EXPOSE 4000", 149 | "RUN apt-get update && apt-get install somepackage", 150 | ], 151 | dockerator_source_dirs: ["config", "lib", "rel", "priv", "web", "extra"], 152 | dockerator_base_image: [build: "elixir:1.4.5", release: "ubuntu:xenial"], 153 | dockerator_target_image: "myaccount/my_app", 154 | ] 155 | end 156 | 157 | def application do 158 | [extra_applications: [:logger], mod: {MyApp, []}] 159 | end 160 | 161 | defp deps do 162 | [ 163 | {:dockerator, "~> 1.3", runtime: false}, 164 | ] 165 | end 166 | end 167 | ``` 168 | """ 169 | end 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dockerator for Elixir 2 | 3 | [![Hex.pm](https://img.shields.io/hexpm/v/dockerator.svg)](https://hex.pm/packages/dockerator) 4 | [![Hex.pm](https://img.shields.io/hexpm/dt/dockerator.svg)](https://hex.pm/packages/dockerator) 5 | 6 | Tool for turning Elixir apps into Docker images without a pain. 7 | 8 | ## Rationale 9 | 10 | One may say that creating a Dockerfile for an Elixir app is so easy that 11 | creating a separate tool for such purpose is an overkill. 12 | 13 | However, that might be not that easy if: 14 | 15 | * You need to maintain a lot of apps and you want to ensure thay use the same 16 | build system without manually replicating tons of Dockerfiles. 17 | * You use dependencies stored on private git repositories. 18 | 19 | In such cases Dockerator will save you a lot of time. 20 | 21 | ## Features 22 | 23 | * **Clean build environment** - It always builds the release of Elixir project 24 | in the clean environment in order to ensure repeatable builds. 25 | * **No source code in the image** - The target image will not contain the 26 | source code, just the compiled release. 27 | * **SSH agent forwarding** - It can handle SSH agent forwarding so you can use 28 | dependencies stored at private SSH repositories without exposing your 29 | credentials. 30 | * **Does not run as root** - By default it does not run application inside 31 | the container as root for additional layer of security. 32 | 33 | Internally it uses [Distillery](https://github.com/bitwalker/distillery) for 34 | building the actual release. 35 | 36 | 37 | # Prerequisities 38 | 39 | You need to use Elixir >= 1.4. 40 | 41 | You need to have on your computer a [Docker](https://docker.io) installation. 42 | The `docker` command should be callable without `sudo`. 43 | 44 | 45 | # Usage 46 | 47 | Add it to the dependencies, by adding the following the `deps` in `mix.exs`: 48 | 49 | ```elixir 50 | def deps do 51 | [ 52 | {:dockerator, "~> 1.3", runtime: false}, 53 | ] 54 | end 55 | ``` 56 | 57 | Moreover add the key `:dockerator_target_image` to the `app` with name of the 58 | target Docker image. 59 | 60 | Then fetch the dependencies: 61 | 62 | ```bash 63 | mix deps.get 64 | ``` 65 | 66 | Create release configuration (if it is not present yet): 67 | 68 | ```bash 69 | mix release.init 70 | ``` 71 | 72 | It will create `rel/` directory, add it to git: 73 | 74 | ```bash 75 | git add rel/ 76 | ``` 77 | 78 | 79 | Then you can just call the following command each time you need to assemble 80 | a Docker image tagged as `latest`: 81 | 82 | ```bash 83 | mix dockerate 84 | ``` 85 | 86 | If you want to make the actual release, please increase version in the 87 | `mix.exs` (potentially you want to also tag the code in git) and then run 88 | 89 | ```bash 90 | mix dockerate release 91 | ``` 92 | 93 | The Docker image will use version from `mix.exs` as a tag. 94 | 95 | You probably want to also change the Mix environment, just prefix the 96 | commands with MIX_ENV=env, e.g.: 97 | 98 | 99 | ```bash 100 | MIX_ENV=prod mix dockerate release 101 | ``` 102 | 103 | 104 | 105 | # Configuration 106 | 107 | You can use the following settings in the `project` of the `mix.exs` in order 108 | to configure Dockerator: 109 | 110 | * `:dockerator_target_image` - (mandatory) - a string containing target 111 | Docker image name, e.g. `"myaccount/my_app"`. 112 | * `:dockerator_base_image` - (optional) - a string or keyword list containing 113 | name of a base Docker image name used for build and release. If it is 114 | a string, it will use provided name for both build and release. If it is 115 | a keyword list, you can specify two keys `:build` or/and `:release` to 116 | specify different images for these two phases. Defaults to 117 | `elixir:latest`. It is strongly encouraged to change this to the particular 118 | [Elixir version](https://hub.docker.com/r/library/elixir/tags/) to have 119 | repeatable builds. 120 | * `:dockerator_run_as_root` - (optional) - a booleaen indicating whether the 121 | application inside the container should be run as root. Defaults to `false`. 122 | * `:dockerator_ssh_agent` - (optional) - a boolean indicating whether 123 | we should use SSH agent for the build. Defaults to `false`. Turn it on 124 | if you're using dependencies that are hosted on private git/SSH repositories. 125 | * `:dockerator_source_dirs` - (optional) - a list of strings containing a list 126 | of source directories that will be copied to the build image. Defaults to 127 | `["config", "lib", "rel", "priv", "web"]`. 128 | * `:dockerator_build_extra_docker_commands` - optional - a list of strings that 129 | will contain extra commands that will be added to the release image. For 130 | example you can add something like `["apt-get install something"]`. 131 | * `:dockerator_release_extra_docker_commands` - optional - a list of strings that 132 | will contain extra commands that will be added to the release image. For 133 | example you can add something like `["EXPOSE 4000"]`. 134 | 135 | ## Example 136 | 137 | For example your `mix.exs` might look like this after the changes: 138 | 139 | ```elixir 140 | defmodule MyApp.Mixfile do 141 | use Mix.Project 142 | 143 | def project do 144 | [app: :my_app, 145 | version: "0.1.0", 146 | elixir: "~> 1.4", 147 | build_embedded: Mix.env == :prod, 148 | start_permanent: Mix.env == :prod, 149 | deps: deps(), 150 | dockerator_ssh_agent: true, 151 | dockerator_build_extra_docker_commands: [ 152 | "RUN apt-get update && apt-get install somepackage", 153 | ], 154 | dockerator_release_extra_docker_commands: [ 155 | "EXPOSE 4000", 156 | "RUN apt-get update && apt-get install somepackage", 157 | ], 158 | dockerator_source_dirs: ["config", "lib", "rel", "priv", "web", "extra"], 159 | dockerator_base_image: [build: "elixir:1.4.5", release: "ubuntu:xenial"], 160 | dockerator_target_image: "myaccount/my_app", 161 | ] 162 | end 163 | 164 | def application do 165 | [extra_applications: [:logger], mod: {MyApp, []}] 166 | end 167 | 168 | defp deps do 169 | [ 170 | {:dockerator, "~> 1.3", runtime: false}, 171 | ] 172 | end 173 | end 174 | ``` 175 | 176 | 177 | # Limitations 178 | 179 | * Currently it assumes that release name defined in `rel/config.exs` is 180 | the same as app name defined in the mix.exs. 181 | * It has to rely on base image based on any `apt`-compatible system, such as 182 | Ubuntu or Debian, however this is unimportant if you don't use git-based 183 | dependencies. This is because if it won't find `git` command in the base 184 | image it will invoke `apt-get install git`. 185 | * At the moment it will not handle SSH agent on other platforms than Mac OS X 186 | and Linux with X running. However it should be quite trivial to add others. 187 | 188 | 189 | # Let me believe that Karma returns! 190 | 191 | Developers are humans, too, we also need to pay bills from time to time. If you 192 | wish to repay time and effort that you have saved thanks to this piece of code, 193 | you can click one of this nice, shiny buttons below: 194 | 195 | | Paypal | Bitcoin | Beerpay | 196 | | :----: | :-----: | :-----: | 197 | | [![](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=5BF9TT5YQNU5J) | [![](https://i.imgur.com/dFkg3fw.png)](https://i.imgur.com/5VJeR9h.png)
1LHsmP3odWxu1bzUfe2ydrewArB72XbN7n | [![Go to Beerpay](https://beerpay.io/mspanc/jumbo/badge.svg)](https://beerpay.io/mspanc/jumbo) | 198 | 199 | 200 | # Authors 201 | 202 | Marcin Lewandowski, marcin@saepia.net 203 | 204 | 205 | # License 206 | 207 | MIT 208 | -------------------------------------------------------------------------------- /lib/mix/tasks/dockerate.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Dockerate do 2 | use Mix.Task 3 | 4 | @default_base_image "elixir:latest" 5 | @ssh_agent_image "nardeas/ssh-agent:latest" 6 | @default_source_dirs ["config", "lib", "rel", "priv", "web"] 7 | 8 | @shortdoc "Assemble a Docker image" 9 | 10 | 11 | def run(args) do 12 | # Determine app name 13 | app = Mix.Project.config |> Keyword.get(:app) 14 | version = Mix.Project.config |> Keyword.get(:version) 15 | info "Assembling a Docker image for app #{app} #{version}, env = #{Mix.env}..." 16 | 17 | # Determine release name 18 | # FIXME read this from rel/config.exs 19 | rel_name = app 20 | 21 | 22 | # Determine if we should run as root 23 | run_as_root = 24 | case Mix.Project.config |> Keyword.get(:dockerator_run_as_root, false) do 25 | value when is_boolean(value) -> 26 | value 27 | 28 | other -> 29 | error "Invalid run_as_root setting #{inspect(other)}" 30 | Kernel.exit(:invalid_run_as_root) 31 | end 32 | 33 | # Determine base image 34 | {base_image_build, base_image_release} = 35 | case Mix.Project.config |> Keyword.get(:dockerator_base_image) do 36 | nil -> 37 | {@default_base_image, @default_base_image} 38 | 39 | image when is_binary(image) -> 40 | {image, image} 41 | 42 | image when is_list(image) -> 43 | {Keyword.get(image, :build, @default_base_image), Keyword.get(image, :release, @default_base_image)} 44 | 45 | other -> 46 | error "Invalid base image #{inspect(other)}" 47 | Kernel.exit(:invalid_base_image) 48 | end 49 | info "Using #{base_image_build} as a base Docker image for build phase" 50 | info "Using #{base_image_release} as a base Docker image for release phase" 51 | 52 | 53 | # Determine source directories 54 | source_dirs = 55 | case Mix.Project.config |> Keyword.get(:dockerator_source_dirs) do 56 | nil -> 57 | @default_source_dirs 58 | 59 | other when is_list(other) -> 60 | other 61 | 62 | other -> 63 | error "Invalid source dirs #{inspect(other)}" 64 | Kernel.exit(:invalid_source_dirs) 65 | end 66 | info "Using #{inspect(source_dirs)} as a list of source directories" 67 | 68 | 69 | # Determine target tag 70 | target_tag = case args do 71 | ["release"] -> 72 | version 73 | 74 | [] -> 75 | "latest" 76 | 77 | other -> 78 | error "Invalid argument #{inspect(other)}, please pass \"release\" or leave it blank" 79 | Kernel.exit(:invalid_argument) 80 | end 81 | 82 | 83 | # Determine target image 84 | target_image = 85 | case Mix.Project.config |> Keyword.get(:dockerator_target_image) do 86 | nil -> 87 | error "Target image is unset, please set :dockerator_target_image in the app's config in mix.exs" 88 | Kernel.exit(:unset_target_image) 89 | 90 | image when is_binary(image) -> 91 | image 92 | 93 | other -> 94 | error "Invalid target image #{inspect(other)}" 95 | Kernel.exit(:invalid_target_image) 96 | end 97 | target_image_build = "#{target_image}/build:#{target_tag}" 98 | target_image_release = "#{target_image}:#{target_tag}" 99 | 100 | info "Using #{target_image_release} as a target Docker image" 101 | 102 | 103 | # Determine extra build commands 104 | build_extra_docker_commands = 105 | case Mix.Project.config |> Keyword.get(:dockerator_build_extra_docker_commands) do 106 | nil -> 107 | [] 108 | 109 | other when is_list(other) -> 110 | info "Using #{inspect(other)} as a list of extra commands for the build Docker image" 111 | other 112 | 113 | other -> 114 | error "Invalid extra build commands #{inspect(other)}" 115 | Kernel.exit(:invalid_build_extra_docker_commands) 116 | end 117 | 118 | # Determine extra release commands 119 | release_extra_docker_commands = 120 | case Mix.Project.config |> Keyword.get(:dockerator_release_extra_docker_commands) do 121 | nil -> 122 | [] 123 | 124 | other when is_list(other) -> 125 | info "Using #{inspect(other)} as a list of extra commands for the release Docker image" 126 | other 127 | 128 | other -> 129 | error "Invalid extra release commands #{inspect(other)}" 130 | Kernel.exit(:invalid_release_extra_docker_commands) 131 | end 132 | 133 | 134 | # Determine templates' path 135 | templates_path = 136 | Mix.Project.deps_paths[:dockerator] 137 | |> Path.join("priv") 138 | |> Path.join("templates") 139 | 140 | 141 | # Determine build path 142 | build_path = 143 | Mix.Project.build_path 144 | |> Path.join("dockerator") 145 | 146 | build_output_path = 147 | build_path 148 | |> Path.join("output") 149 | 150 | build_output_path_relative = 151 | build_output_path 152 | |> Path.relative_to_cwd 153 | 154 | build_scripts_path = 155 | build_path 156 | |> Path.join("scripts") 157 | 158 | 159 | # Create directory for build files 160 | with \ 161 | :ok <- File.mkdir_p(build_path), 162 | {:ok, _} <- File.rm_rf(build_output_path), 163 | :ok <- File.mkdir_p(build_output_path), 164 | :ok <- File.mkdir_p(build_scripts_path) 165 | do 166 | info "Using #{build_path} as a temporary build path" 167 | 168 | else 169 | e -> 170 | error "Failed to create build directory #{build_path}: #{inspect(e)}" 171 | Kernel.exit(:failed_build_directory) 172 | end 173 | 174 | # Check if we're using any git dependencies 175 | git_deps_urls = 176 | Mix.Dep.Lock.read 177 | |> Map.values 178 | |> Enum.filter(fn 179 | {:git, _git_url, _, _} -> true 180 | _ -> false 181 | end) 182 | |> Enum.map(fn({_, dep_url, _, _}) -> 183 | case URI.parse(dep_url) do 184 | %URI{scheme: nil} -> 185 | "ssh://#{dep_url}" 186 | 187 | other -> 188 | other 189 | end 190 | end) 191 | |> Enum.uniq_by(fn(git_dep_url) -> 192 | case URI.parse(git_dep_url) do 193 | %URI{scheme: "ssh", host: host, port: nil} -> 194 | {host, 22} 195 | %URI{scheme: "ssh", host: host, port: port} -> 196 | {host, port} 197 | other -> 198 | other 199 | end 200 | end) 201 | 202 | case git_deps_urls do 203 | [] -> 204 | info "Found no git dependencies" 205 | 206 | other -> 207 | info "Found the following git dependencies:" 208 | 209 | Enum.each(other, fn(git_dep_url) -> 210 | info " #{git_dep_url}" 211 | end) 212 | end 213 | 214 | 215 | # Generate scripts from templates 216 | dockerfile_build = 217 | Path.join(templates_path, "build.Dockerfile.eex") 218 | |> EEx.eval_file([ 219 | base_image: base_image_build, 220 | mix_env: Mix.env, 221 | git_deps_urls: git_deps_urls, 222 | source_dirs: source_dirs, 223 | build_extra_docker_commands: build_extra_docker_commands 224 | ]) 225 | 226 | dockerfile_release = 227 | Path.join(templates_path, "release.Dockerfile.eex") 228 | |> EEx.eval_file([ 229 | base_image: base_image_release, 230 | mix_env: Mix.env, 231 | build_output_path_relative: build_output_path_relative, 232 | release_extra_docker_commands: release_extra_docker_commands, 233 | rel_name: rel_name, 234 | run_as_root: run_as_root 235 | ]) 236 | 237 | # Remove empty lines in Dockerfile as they're deprecated 238 | dockerfile_build = Regex.replace(~r/\n+/, dockerfile_build, "\n") 239 | dockerfile_release = Regex.replace(~r/\n+/, dockerfile_release, "\n") 240 | 241 | dockerfile_build_path = 242 | build_scripts_path 243 | |> Path.join("build.Dockerfile") 244 | 245 | dockerfile_release_path = 246 | build_scripts_path 247 | |> Path.join("release.Dockerfile") 248 | 249 | with \ 250 | :ok <- File.write(dockerfile_build_path, dockerfile_build), 251 | :ok <- File.write(dockerfile_release_path, dockerfile_release) 252 | do 253 | info "Succesfully generated build scripts" 254 | 255 | else 256 | e -> 257 | error "Failed to generate build scripts: #{inspect(e)}" 258 | Kernel.exit(:failed_build_scripts) 259 | end 260 | 261 | 262 | # Check if we need a SSH agent 263 | ssh_agent = 264 | case Mix.Project.config |> Keyword.get(:dockerator_ssh_agent) do 265 | nil -> 266 | false 267 | 268 | other when is_boolean(other) -> 269 | other 270 | 271 | other -> 272 | error "Invalid SSH agent setting #{inspect(other)}" 273 | Kernel.exit(:invalid_ssh_agent) 274 | end 275 | 276 | ssh_agent_docker_name = 277 | String.replace(target_image, "/", "_") <> "-sshagent" 278 | 279 | if ssh_agent do 280 | case System.cmd "docker", ["inspect", "-f", "'{{.State.Running}}'", ssh_agent_docker_name] do 281 | {"'true'\n", 0} -> 282 | info "SSH agent seems to be already running" 283 | info " If you want to clean it, kill it by invoking the following command:" 284 | info " docker rm #{ssh_agent_docker_name} -f" 285 | 286 | {"'false'\n", 0} -> 287 | info "SSH agent seems to be present but not running, starting" 288 | case docker_cmd_passthrough ["start", ssh_agent_docker_name] do 289 | :ok -> 290 | ssh_agent_add_keys!(ssh_agent_docker_name) 291 | 292 | {:error, code} -> 293 | error "Docker run returned error code #{code}" 294 | Kernel.exit(:failed_docker_start_ssh_agent) 295 | end 296 | 297 | {"\n", 1} -> 298 | info "Starting SSH agent (#{ssh_agent_docker_name})" 299 | case docker_cmd_passthrough ["run", "-d", "--name=#{ssh_agent_docker_name}", @ssh_agent_image] do 300 | :ok -> 301 | ssh_agent_add_keys!(ssh_agent_docker_name) 302 | 303 | {:error, code} -> 304 | error "Docker run returned error code #{code}" 305 | Kernel.exit(:failed_docker_run_ssh_agent) 306 | end 307 | end 308 | end 309 | 310 | 311 | # Phase 1: Prepare build image 312 | info "Building image for the build phase (#{target_image_build})" 313 | case docker_cmd_passthrough ["build", "-t", target_image_build, "-f", dockerfile_build_path, "."] do 314 | :ok -> 315 | info "Built image for the build phase" 316 | 317 | {:error, code} -> 318 | error "Docker build returned error code #{code}" 319 | Kernel.exit(:failed_docker_build) 320 | end 321 | 322 | 323 | # Phase 2: Prepare release 324 | info "Building release" 325 | 326 | release_docker_extra_args = if ssh_agent do 327 | ["--volumes-from=#{ssh_agent_docker_name}", "-e", "SSH_AUTH_SOCK=/.ssh-agent/socket"] 328 | else 329 | [] 330 | end 331 | 332 | # FIXME 333 | # This contains a workaround for https://github.com/bitwalker/distillery/issues/314 334 | # which is probably a bug in bundlex, but anyway doing sequence of 335 | # mix compile / rm -rf _build / mix release guarantees that NIFs are copied 336 | # into release. 337 | release_docker_args = ["run"] ++ 338 | release_docker_extra_args ++ 339 | [ 340 | "--mount", "type=bind,source=#{build_output_path},target=/root/output", 341 | "--rm", 342 | "-t", target_image_build, 343 | "sh", "-c", "(mix deps.get && mix compile && rm -rf _build && mix release && mv -v _build/#{Mix.env}/rel/#{rel_name} /root/output/app/)" 344 | ] 345 | 346 | case docker_cmd_passthrough release_docker_args do 347 | :ok -> 348 | info "Built release" 349 | 350 | {:error, code} -> 351 | error "Release returned error code #{code}" 352 | Kernel.exit(:failed_release) 353 | end 354 | 355 | 356 | # Phase 3: Prepare release image 357 | info "Building image for the release phase (#{target_image_release})" 358 | case docker_cmd_passthrough ["build", "-t", target_image_release, "-f", dockerfile_release_path, "."] do 359 | :ok -> 360 | info "Built image for the release phase" 361 | 362 | {:error, code} -> 363 | error "Docker build returned error code #{code}" 364 | Kernel.exit(:failed_docker_build) 365 | end 366 | 367 | 368 | info "Done. Your app is bundled in the image #{target_image_release}." 369 | end 370 | 371 | 372 | defp ssh_agent_add_keys!(ssh_agent_docker_name) do 373 | case :os.type do 374 | {:unix, subtype} -> 375 | info "Adding your SSH keys to the SSH agent." 376 | info " Please type password for your SSH keys in the new Terminal window if they're password-protected." 377 | 378 | # This hack is necessary because it is very hard to call 379 | # PTY-enabled command from Elixir/Erlang, so we just spawn new 380 | # Terminal window in case of users' SSH keys are 381 | # password-protected. 382 | # 383 | # The last line kills the terminal window so we don't wait for 384 | # Command+Q. 385 | tmp_script_path = "/tmp/#{ssh_agent_docker_name}.sh" 386 | tmp_script_body = """ 387 | #!/bin/sh 388 | docker run --rm --volumes-from=#{ssh_agent_docker_name} -v ~/.ssh:/.ssh -it #{@ssh_agent_image} ssh-add /root/.ssh/id_rsa 389 | sleep 1 390 | kill -9 $(ps -p $(ps -p $PPID -o ppid=) -o ppid=) 391 | """ 392 | 393 | File.write!(tmp_script_path, tmp_script_body) 394 | File.chmod!(tmp_script_path, 0o700) 395 | 396 | case subtype do 397 | :darwin -> 398 | System.cmd "open", ["-W", "-a", "Terminal.app", tmp_script_path] 399 | 400 | :linux -> 401 | System.cmd "x-terminal-emulator", ["-hold", "-e", tmp_script_path] 402 | 403 | other -> 404 | error "TODO: This operating system subtype (#{inspect(other)}) is not supported yet" 405 | Kernel.exit(:todo) 406 | end 407 | 408 | File.rm!(tmp_script_path) 409 | 410 | other -> 411 | error "TODO: This operating system (#{inspect(other)}) is not supported yet" 412 | Kernel.exit(:todo) 413 | end 414 | end 415 | 416 | 417 | defp docker_cmd_passthrough(args) do 418 | case System.cmd("docker", args, into: IO.binstream(:stdio, :line)) do 419 | {_, 0} -> 420 | :ok 421 | 422 | {_, code} -> 423 | {:error, code} 424 | end 425 | end 426 | 427 | 428 | defp info(message) do 429 | Mix.Shell.IO.info "[Dockerator INFO] #{message}" 430 | end 431 | 432 | 433 | defp error(message) do 434 | Mix.Shell.IO.error "[Dockerator ERROR] #{message}" 435 | end 436 | end 437 | --------------------------------------------------------------------------------