Docker - SolarisJapan/lunaris-wiki GitHub Wiki
Dockerization Guide
This guide is meant to provide a resource for Dockerizing / containerizing Elixir and Phoenix apps.
Example Tech-Stack
Elixir, Phoenix, Ecto, Postgresql, and Redis for app development.
Docker (including Docker Desktop and CLI), Distillery, and Digital Ocean with Ubuntu 18.04 for deployment.
Docker images include Alpine-Linux, Alpine-Elixir, Nginx, Redis, and Postgres-Alpine.
Installation
Install Docker Desktop and CLI.
Setup and initialize your release with the Distillery Elixir dependency. A release does not need to be created yet but can be done to confirm functionality.
Distillery documentation also has guides for deploying to Digital Ocean with Docker if stuck. The files and help in this guide can be referenced against it.
Releases and Distillery Configuration
The configuration should be pretty straight forward and can be done as shown in the Distillery documentation. The initialization process should begin from the app's root directory for an umbrella app. Here are some things to be sure were covered for "distillation" of a release.
Umbrella Apps
For an umbrella app, the following line in the umbrella app config includes the config of each dependency app: import_config "../apps/*/config/config.exs"
.
Umbrella apps should also ensure that the app's root directory mix.exs
file has a version number and app name just like those found in the dependency app mix.exs
files. See mix.exs
in the app root for an example.
See Distillery documentation for umbrella apps if each dependency is meant to be maintained and released independently.
Docker Setup and Configuration
Create the necessary files for Docker configuration.
touch Dockerfile
touch Makefile
touch .dockerignore
touch docker-compose.yml
mkdir -p config && touch config/docker.env
Some initial content for these files can be obtained from Distillery documentation.
Note that the Makefile should be indented with tabs not spaces. In Atom press command + shift + p
and search for "Whitespace: Convert Spaces to Tabs". Running this command will quickly ensure the whitespace is properly formatted.
Docker Usage
Familiarize yourself with the concepts of containers, images, etc with the Docker documentation. Their purpose and use will be more apparent as you continue to dockerize your app.
As you build, debug, and edit your images, you may start to accumulate unused images and volumes. These should be pruned from time to time to avoid significant wasted drive space by running docker image prune
.
Environment Variables
Edit rel/config.exs
to include Mix Config Providers.
Once config_providers
and overlays
are set in rel/config.exs
(as done early in Distillery Configuration) environment variables can be set via config/docker.env
if desired by adding the following line to Dockerfile
:
RUN \
...
# mix release --verbose && \ # old release command
env `cat config/docker.env | grep -v '^\s*#'` mix release --verbose && \
...
Of course the names of the environment variables found in your apps' config/prod.exs
file(s) need to match. Note also that quotes in this file are escaped when inserted into the environment and should likely be left out.
Without this line environment variables found in the Docker Env are only used for run-time configuration. Files such as those for production configuration are compiled on release and do not reflect changes to this file. Any desired change in these variables requires a new release to be "distilled".
Run make build
when ready to ensure that all these files work together to make a Dockerized image of your app release.
Database Setup and Migrations
Distillery documentation environment variables for the database can be edited slightly to make the database setup easier. With the right environment variable names the Docker Postgresql image will generate a database in it's volume when first created.
Renaming the environment variables in config/docker.env
will likely make the setup much easier.
POSTGRES_HOST=db # should be the name of the postgres image
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
PGPASSWORD=postgres # same as the above password
POSTGRES_DB=docker_guide_db # name of the first database
PGPASSWORD
seems to be redundant, but this variable is used by the psql cli application to authenticate commands without a prompt. Toward the bottom of the Dockerfile
add the following if it has not yet been included.
RUN apk update && \
apk add --no-cache \
bash \
openssl-dev \
postgresql-client # add this line to enable psql within the container
Set up migrations to occur when your containers are started.
Apps with Multiple Databases
This Docker container named "admin" set in docker-compose.yml
starts the process of adding an additional database and migrations for each via this line: command: ["/opt/app/bin/loyalty", "setup"]
.
In order to ensure this setup file is found in the container, copy this script to the same location in your project and edit it to your needs.
This script first ensures that the Postgresql database is open to connections, retrying a set number of times before timing out. If the database connects, it then creates the additional database and runs migrations for the dependency apps.
Using Docker Compose
As with the Docker CLI, you will also frequently use the Docker Compose CLI.
At this point the main goal should be to have make build
successfully creating images of your app. The setup and configuration for Docker compose should also be to a point where you can start creating containers with your images.
If make build
successfully creates an image and docker-compose up
creates a container for that image and it's dependencies, then it's time to deploy.
Server Preparation
At this point the release should successfully compile and docker-compose up
allows you to see your app on the local machine (unless already configured otherwise).
Create a server for use with the application and do any initial setup for the server.
Create a folder for the application on the server and copy the files used for docker-compose
.
# from server
mkdir -p /etc/dockerization_guide/config
# from local machine
scp ./docker-compose.yml [email protected]:/etc/dockerization_guide
scp ./config/docker.env [email protected]:/etc/dockerization_guide/config/docker.env
Dockerhub
In order to move our dockerized release to the server we can use Dockerhub and the Docker client application. In this example we have called our app "dockerization_guide" and are releasing version 0.1.0. After a successful make build
we can run the following:
docker push dockerization_guide:0.1.0
# Or with an organization
docker push organization/dockerization_guide:0.1.0
Once this completes we simply run the following from the server
docker pull dockerization_guide:0.1.0
# Or with an organization
docker pull organization/dockerization_guide:0.1.0
If you are tagging images with latest and deploying that tag instead (not recommended), deploying an update this way may not work, as the image will not be refreshed if it has already been pulled. You can force an upgrade like so:
- Pull the latest image
docker pull username/something/app1.0.0
- On the Server Change directory to where the docker-compose.yml exists
/etc/app/
- Stop the current running Containers
docker-compose down
- And start back up the Containers
docker-compose up
SSL: Certbot and Nginx
Next we want to set up automatically renewing SSL certificates and proxying. Follow along and reference against the example Nginx and Docker Compose config files to see the adjustments made. Note also that if used in conjunction with Cloudflare's DNS the DNS record's "Status" field was set to "DNS only" since we are handling HTTPS redirects.
Pay close attention to the ports specified in docker-compose.yml
as they are somewhat altered from the original Distillery guide. Note that Nginx was set to be dependent upon the web app.
/etc/letsencrypt/options-ssl-nginx.conf
was not included by default and was found from the Let's Encrypt Github (included in data/nginx/options-ssl-nginx.conf
, copy to server in appropriate location)
Deployment
Once the project has been pulled onto the server and Nginx has been set up, docker can be set to automatically start.
The following enables docker to start when the server restarts.
sudo systemctl enable docker
Once everything is set up you can run the following from the folder where the docker-compose.yml
file is:
docker-compose up -d
The -d
flag daemonizes the process and it will also be restarted if the docker service has been enabled to restart via systemctl. Remove this flag to see the server output directly for debugging.
Interfacing as a Developer
With the current setup, any access to the app or its databases need to occur through Docker. The volumes where the data is stored are in a somewhat hidden location from the user (as a developer).
Moreover, you needn't have installed a programming environment on the server for Docker to work, so no applications such as iex or mix are available on the server. Here are some ways to interface with your Docker app.
Docker Exec
From within the server, docker exec can be used to interface with the container via command line.
# first find CONTAINER - the id of the container - for docker exec docker container ps container
docker exec -it CONTAINER COMMAND
### Useful commands
# Access local postgres database with a password
docker exec -it CONTAINER psql "user=postgres dbname=database host=db password=postgres"
# Access the Elixir app via console
docker exec -it CONTAINER sh
cd bin/
./myapp remote_console
Docker Logs
From within the server, docker logs can be used to see the container log output for debugging and monitoring.
NOTE: docker logs
without specifying any "tail length" will print all available log information and is probably excessive.
# first find CONTAINER - the id of the container - for docker logs docker container ps container
docker logs CONTAINER
### Useful commands
# See the last 30 lines of the logs
docker logs --tail 30 CONTAINER
# See the logs as they are updated live (trimmed to 30 lines)
docker logs --follow --tail 30 CONTAINER
pgAdmin 4
The setup for pgAdmin 4 is very quick. From the pgAdmin client select "Object" > "Create" > "Server...". Under the "Connections" tab set the "Hostname/address", "Port", "Username", and "Password" with the same settings defined on the server (via docker.env).
"Maintenance Database" can be left as the default "postgres" unless changed previously. Under the "Advanced" tab, set "Host address" as the IP address of the server (without url scheme, port, etc.). After saving the database should be accessible.
Areas for Improvement
Docker swarm may eventually be a desired next step for the deployment setup. Currently if the server is restarted, Docker Compose starts the containers when docker is restarted thanks to systemctl.
Currently Nginx error logging is not working as intended. There should be a file specified in the config file matching that found in docker-compose.yml
where errors can be logged, but it was not working properly when attempted.
Maintaining version numbers for future releases, particularly for umbrella apps, may be something of a hassle to maintain in multiple locations. Maybe specifying docker_guide:latest
in the right locations makes this easier. Version number is automaticaly configured from the mix.exs file.
MyPhoenixApp.Endpoint
config (in conjunction with respective fields in docker-compose.yml
) may need to be altered for a more appropriate and secure web / ssl setup. Currently the app is still using localhost
of it's container for production host.
Phoenix's default cache_manifest.json
file created by mix digest
and defined in the production config was having trouble being found during docker build
(make build
). This may be useful or even necessary for some apps to function properly.