Posted on 2017-07-05 14:44:48
This post is aimed towards developers who have developed a Phoenix application that they want to deploy to a staging or production system. To ensure deployment to production systems is reliable, repeatable and scalable I am using docker and the mix docker library which uses distillery to package your application in a production environment within a docker image.
The first steps if this deployment are largely covered excellently by this blog post which will get you to the point where your docker images have been created.
However, I have added a few steps so that my database creation, migration and seed commands are run when my application starts. Before starting step 5 and running mix docker.build
and mix docker.release
you need to configure your release so that you can add your migration and seed functionality.
To do this run the following command to generate your release files.
mix release.init
This will create a rel
directory within which you are going to need to make a few changes. You need to create pre and post start boot hooks which will run your commands. To do this open up rel/config.exs
and change the environment :prod
function to the following.
environment :prod do
set include_erts: true
set include_src: false
set cookie: :"GENERATED_SECRET"
set pre_start_hook: "rel/hooks/pre_start"
set post_start_hook: "rel/hooks/post_start"
end
You then need to create the rel/hooks/pre_start
and rel/hooks/post_start
shell command files. The pre start file will run Elixir.Release.Tasks createdb
which creates your database if it doesn’t already exist and the post start script will check when the application has fully started and then call the Elixir.Release.Tasks migrate
command from your release binary.
Your pre start script is as follows:
#rel/hooks/pre_start
echo "Running database creation"
bin/my_app command Elixir.Release.Tasks createdb
echo "DB created successfully"
And the post start script:
#rel/hooks/post_start
set +e
while true; do
nodetool ping
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
echo "Application is up!"
break
fi
done
set -e
echo "Running migrations"
bin/my_app rpc Elixir.Release.Tasks migrate
echo "Migrations run successfully"
If you were to run this now it would cause an error as the Elixir.Release.Tasks
commands haven’t been written yet. To do this you need to create a file in the lib
directory, for example lib/release.ex
which will call the database creation and migration commands for your application and run a seed script if you have one created at priv/repo/seeds.exs
. Your release script should be as follows.
defmodule Release.Tasks do
@start_apps [
:postgrex,
:ecto
]
@myapps [
:my_app
]
@repos [
MyApp.Repo
]
def createdb do
# Ensure all apps have started
Enum.each(@myapps, fn(x) ->
:ok = Application.load(x)
end)
# Start postgrex and ecto
Enum.each(@start_apps, fn(x) ->
{:ok, _} = Application.ensure_all_started(x)
end)
# Create the database if it doesn't exist
Enum.each(@repos, &ensure_repo_created/1)
:init.stop()
end
def migrate 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 myapp
IO.puts "Starting repos"
Enum.each(@repos, &(&1.start_link(pool_size: 1)))
# Run migrations
Enum.each(@myapps, fn(myapp) ->
run_migrations_for(myapp)
# Run the seed script if it exists
seed_script = seed_path(myapp)
if File.exists?(seed_script) do
IO.puts "Running seed script for #{myapp}"
Code.eval_file(seed_script)
end
end)
end
def priv_dir(app), do: "#{:code.priv_dir(app)}"
defp run_migrations_for(app) do
IO.puts "Running migrations for #{app}"
Ecto.Migrator.run(MyApp.Repo, migrations_path(app), :up, all: true)
end
defp migrations_path(app), do: Path.join([priv_dir(app), "repo", "migrations"])
defp seed_path(app), do: Path.join([priv_dir(app), "repo", "seeds.exs"])
defp ensure_repo_created(repo) do
case repo.__adapter__.storage_up(repo.config) do
:ok -> :ok
{:error, :already_up} -> :ok
{:error, term} -> {:error, term}
end
end
end
Once these steps have been done you can proceed with the remaining steps in Alex Kovshovik’s post to generate the docker images and you’ll almost be ready to deploy on your server. The only other change I had to make was to add the --net=host
flag when running the docker container so that the container could access postgres on the host machine.
For the deployment I am assuming that this will be a lightweight application that will be running with everything on one server with the phoenix application contained within a single docker container. Deployment to a large scale system across multiple servers is beyond the scope of this post but is covered well by Tymon Tobolski.
To deploy your system on a server you will first need to have an account and repo (either private or public) on docker.com so that we can push and pull our images. You may also need to run docker login
in order to access your repository.
You will firstly need to tag your images, obviously you’ll need to use your own docker username and repository name.
docker tag my_app:build USERNAME/REPO_NAME:build && docker tag my_app:release USERNAME/REPO_NAME:release
Then you can push your images to the docker cloud.
docker push
Once they have successfully been pushed you can SSH to your server which should have postgres and docker installed. You will only need to pull the release image to your server. Again you may need to run docker login
on your server before running:
docker pull USERNAME/REPO_NAME:release
Run the following command to start the container replacing the environment variables with your domain name, database credentials and a secret key which can be generated using mix.gen.secret
.
docker run -d --net=host -p 4000:4000 -e PORT=4000 -e HOST=<your-domain-name> -e DB_HOST=localhost -e DB_NAME=<your=db-name> -e DB_USER=<your-db-user> -e DB_PASSWORD=<your-db-password> -e SECRET_KEY_BASE=<your-secret-key> USERNAME/REPO_NAME:release foreground
The application should now be running. To forward traffic to it you will need to configure nginx. My nginx configuration looks like this.
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://localhost:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Once nginx is configured and restarted you should be up and running.
Comments