Dockerized Phoenix Production Migrations
Problem
On production, I have a phoenix web application running in a docker container. This container is connected to a postgres database container via a docker compose. I’d like to be able to run the migration for a new image before updating the running image.
Solution
I created a script that will run the migrations in a side container without using docker compose. This allows migrations to be run for an image that exists in the docker cache but may not be running. Example workflow:
# build the docker image locally
$ docker build -t mcordell/notes:0.2.1 .
# push to docker image to production cache
$ docker save mcordell/notes:0.2.1 | ssh REMOTE_HOST 'docker load'
$ ssh REMOTE_HOST
$(production) ./migrate_app mcordell/notes:0.2.1
Before the script can be created, a module has to be created in the application to run the migrations since we do not have mix:
defmodule Notes.Release do
@app :notes
def migrate do
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
def rollback(repo, version) do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
defp repos do
Application.load(@app)
Application.fetch_env!(@app, :ecto_repos)
end
end
With this in place we can then run migrations by running Notes.Release.migrate
. We can now create a bash script that will load run that code in a passed image:
#! /bin/bash
# Production script for migrating
source .env
docker run -it \
--network root_backend \
-e MIX_ENV="prod" \
-e APP_PORT=5000 \
-e DB_HOST="db" \
-e DB_USER="${DB_USER}" \
-e DB_PASSWORD="${DB_PASS}" \
-e APP_DATABASE="$DB_NAME" \
-e SECRET_KEY_BASE="${SECRET_KEY_BASE}" \
$1 /app/bin/notes eval "App.Release.migrate"
Note that this script is passing necessary environmental variables and connecting to the network specified in the docker-compose (see below). Also, this script assumes the built binary resides at /app/bin/app_name
within the docker container. Make the script executable with chmod +x
and then it can be used like:
./migrate_app IMAGE_NAME
Context
version: "3"
services:
db:
image: postgres:12.3-alpine
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_HOST=db
- POSTGRES_DB=${DB_NAME}
volumes:
- /data/pg:/var/lib/postgresql/data
networks:
- backend
app:
image: mcordell/notes:0.2.0
expose:
- 5000
environment:
HTTP_PORT: 5000
MIX_ENV: prod
APP_PORT: 5000
DB_HOST: db
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASS}
APP_DATABASE: ${DB_NAME}
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
working_dir: /app
networks:
service_network:
backend:
command: /app/bin/notes start
depends_on:
- db
networks:
service_network:
backend:
driver: bridge
internal: true