
Am writing this article to explain how I was able to Deploy Phoenix 1.4 to Digital Ocean. This article will be a combination of two articles and some of my knowledge.
Reference articles
- Deploying Elixir applications with Docker and Digital Ocean
- Flexible Dockerized Phoenix Deployments.md
Building the docker image.
In this step I chose to use the Builder Pattern discribed on the first blog link, where we have two docker containers one to build the image and the other to build a release to ensure that we dont get errors when we deploy the image in a different OS.To achieve this, we will use mix_docker a dependency that make this process very painless.
Since mix_docker has not been maintain for over two year it is not compartible with phoenix 1.4.To solve this I forked it and updated distillery to version 2.0 and changed cowboy to plug_cowboy version 2.0.
Now let add my updated dependency to deps.
1 2 3 4 5 | defp deps do [ {:mix_docker, github: "johninvictus/mix_docker" } ] end |
Now let continue, initiate the process of building the image by running this command on your terminal inside your project.
1 | mix docker.init |
Configure mix_docker
We plan to push the docker image we build to the docker hub later so, to prepare for that create an account at Docker Hub if you don't have any yet. Create a repository with the name of your project. eg sample.

After you create the repository, add this config to Phoenix config file,
1 2 | # docker mix generated image config :mix_docker, image: "johninvictus/sample" |
To do this write this to your terminal.
1 | mix docker.customize |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | FROM bitwalker /alpine-elixir-phoenix ENV HOME= /opt/app/ TERM=xterm # RUN apk update && apk add bash ENV MIX_ENV prod # Add the files to the image COPY . . # Cache Elixir deps RUN mix deps.get --only prod RUN mix deps.compile WORKDIR assets # Cache Node deps RUN npm i # Compile JavaScript RUN npm run deploy WORKDIR .. # Compile app RUN mix compile RUN mix phx.digest RUN mix release -- env =prod --verbose |
To see if the above code is okay, run this command to build the image.
1 | mix docker.build |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | FROM bitwalker /alpine-elixir-phoenix RUN apk update && apk add bash EXPOSE 4000 ENV PORT=4000 MIX_ENV=prod REPLACE_OS_VARS= true SHELL= /bin/sh ADD sample. tar .gz ./ RUN chown -R default . /releases USER default ENTRYPOINT [ "/opt/app/bin/sample" ] |
Dockerfile.release, file is used to build a release image. To test that everything is okay with it, run this command.
1 | mix docker.release |
1 | mix docker.shipit |
1 | johninvictus /sample :0.1.0.20-aa6219bf13 |

If all goes well, let set up docker compose so that we can set databases and migrations.
Setting up docker-compose
Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration.To start using docker-compose create docker-compose.yml file at the root of your project. Then, add the following content.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | version: "3" services: db: image: postgres:10.2-alpine environment: DB_HOST: 127.0.0.1 POSTGRES_DB: "sample_prod" POSTGRES_USER: "postgres" POSTGRES_PASSWORD: "postgres" DB_PORT: 5432 web: image: "johninvictus/sample:0.1.0.20-aa6219bf13" command : foreground depends_on: - db ports: - "4000:4000" environment: PORT: 4000 POOL_SIZE: 10 |
From the above content, db contains the Postgres container and the initial desired initial configuration. The web section contains your uploaded docker hub repository image and it depends on the database you created. The DATABASE_URL, is of format "ecto://username:password@db/database name"
After that let Phoenix project know about the created database, by extending the configuration. Open your prod.exs or product.secret.exs and make sure the database is pointing to the created database.
1 2 3 4 5 6 7 8 | # Configure your database config :sample, Blog.Repo, adapter: Ecto.Adapters.Postgres, username: "postgres" , password: "postgres" , database: "sample_prod" , pool_size: 15 |
ie
1 2 3 4 | # Configures the endpoint config :sample, SampleWeb.Endpoint, url: [host: "localhost" ], server: true , |
1 | docker-compose up |
Now let add logic to create migrations and seed data.
Running migrations
Since you are using minimal docker build, you will not have tools like Mix to help you run migration for this we will use distillery scripts to do the Seeding and migrating of the database.Create a file called release_tasks.ex at lib/your project name and add the following content.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | defmodule Your project name.ReleaseTasks do @start_apps [ :crypto, :ssl, :postgrex, :ecto_sql ] @repos Application.get_env(:sample, :ecto_repos, []) def migrate(_argv) do start_services() run_migrations() stop_services() end def seed(_argv) do start_services() run_migrations() run_seeds() stop_services() end defp start_services do IO.puts( "Starting dependencies.." ) # Start apps necessary for executing migrations Enum.each(@start_apps, &Application.ensure_all_started /1 ) # Start the Repo(s) for app IO.puts( "Starting repos.." ) Enum.each(@repos, & &1.start_link(pool_size: 10)) end defp stop_services do IO.puts( "Success!" ) :init.stop() end defp run_migrations do Enum.each(@repos, &run_migrations_for /1 ) end defp run_migrations_for(repo) do app = Keyword.get(repo.config, :otp_app) IO.puts( "Running migrations for #{app}" ) migrations_path = priv_path_for(repo, "migrations" ) Ecto.Migrator.run(repo, migrations_path, :up, all: true ) end defp run_seeds do Enum.each(@repos, &run_seeds_for /1 ) end defp run_seeds_for(repo) do # Run the seed script if it exists seed_script = priv_path_for(repo, "seeds.exs" ) if File.exists?(seed_script) do IO.puts( "Running seed script.." ) Code.eval_file(seed_script) end end defp priv_path_for(repo, filename) do app = Keyword.get(repo.config, :otp_app) repo_underscore = repo |> Module. split () |> List.last() |> Macro.underscore() priv_dir = "#{:code.priv_dir(app)}" Path. join ([priv_dir, repo_underscore, filename]) end end |
You can find more about this script here. distillery/2.0.0-rc.1/running_migrations
Now let create scripts to run the task, go to rel/ folder and create hooks folder where we will create a script that is run after the Phoenix application boots or before boot.
Then inside hooks, create another folder called post_start. In this folder, we are going to add all the script files that need to run after the Phoenix project boots.
Inside post_start folder create a file called migrationseed.sh and add the following content.
1 2 3 4 5 6 | ##!/bin echo "Running migrations and seed data if any" # bin/docker_magic migrate release_ctl eval --mfa "Sample.ReleaseTasks.seed/1" --argv -- "$@" echo "Migrations and Seed data run successfully" |
Now let make sure the command is actually run after Phoenix boots by adding the following command to rel/config.
1 2 | environment :prod do set (post_start_hooks: "rel/hooks/post_start" ) |
Deploying with Docker Machine
This the last section in this long article and I believe its the simplest.Here we will use Docker Machine and Digital Ocean driver to deploy our docker image. If you wish to deploy to other services you can choose a different Driver.To start the process of deployment, first, generate an API TOKEN from here. API TOKEN.

After you create the token, copy it to the terminal as an environment variable.
1 | export DIGITAL_OCEAN_TOKEN=your token here |
1 | docker-machine create --driver=digitalocean --digitalocean-access-token=$DIGITAL_OCEAN_TOKEN --digitalocean-size=512mb sample |
1 | docker-machine ssh sample |
You can point your terminal to the remote host using this command.
1 | eval $(docker-machine env sample) |
After doing the above we need to start the docker compose inside Digital ocean by running this command.
1 | docker-compose up -d |
1 | eval $(docker-machine env -u) |
Final thoughts
I know that was a very long article but lemme hope it will be of help to somebody struggling to host his/her project to the cloud. Here is a project I have dockerlized, johninvictus/blog, Hope it helps.One final thing, add this code to prod.exs since adding it, solved a bug I was struggling with.
1 2 | # in production as building large stacktraces may be expensive. config :phoenix, :stacktrace_depth, 20 |
Tutorial: Deploying phoenix 1.4 to Digital Ocean Droplet With Docker
Reviewed by John Invictus
on
08:49
Rating:
