Development environment for Elixir + Phoenix with Docker and Docker-compose
Tags: elixir, docker, postgres, webdev
Published: 2020-08-16 20:09:37

_You can now watch this article as a video:_
[Video link](https://youtu.be/CNVst9k-E8M) **OUTDATED**

**Edit 27.11.2021: Since Phoenix 1.6.0 release removed Webpack, Npm etc. and moved to Esbuild, the article is now updated accordingly.**

**Edit 25.12.2021: Change endpoint ip which works in Docker environment.**

I've been following Elixir, Phoenix and Docker for a two years now. I Started with small demos and experiments. Back then configuring Elixir and Phoenix to Docker environment was a pain, especially the configuration for runtime. There was at least a 20 different ways and tutorials how to configure Elixir and Phoenix for Docker and Docker-compose. Each of those tutorials worked out, but some were outdated or too complicated.

Today I had some spare time to deep dive again into this problem and configure proper hot-reloaded local development environment for Elixir + Phoenix using Docker and Docker-compose.

# Prerequisite

In this article I'm not going to go through installing these tools, but below is a list of what is needed.

* Docker
* Docker-compose
* Elixir (I used latest 1.10.4)
* Phoenix framework (I used latest 1.5.4)

# Creating Phoenix application

Once you have everything installed you're ready to generate new Phoenix project by executing:

```
$ mix phx.new app_name
```

There are a few flags to pass along this command, but this command covers everything we need (Ecto, webpack etc). More on [phx.new documentation](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.New.html#module-options).

##  Deps

During the project bootstrap `mix` will execute command `deps.get & deps.compile` to fetch and compile needed dependencies

This is problematic since we want to run our program/web-server in container, not on bare host system. In order to run Elixir application it needs to be compiled in same system architecture where the release binary will be run on. For now, it's safe to remove `deps` folder from the project structure.

Now lets dive into setting up our Docker configuration for our newly created application!

# Docker and Docker-compose

In this section we will be creating following files:

* Dockerfile
* docker-compose.yml
* .env

`Dockerfile` is used to build Docker image which holds our application, dependencies and needed tools.

`docker-compose.yml` - yaml-markup file to define our services, volumes etc.

`.env` - Holds our environment variables for the application.

## Dockerfile

Below you can see the contents of our Dockerfile.

```docker
FROM bitwalker/alpine-elixir-phoenix:latest

WORKDIR /app

COPY mix.exs .
COPY mix.lock .

CMD mix deps.get && mix phx.server
```

On the first line we define our base image which is used to build our image on to. As you can see, we're using `bitwalker/alpine-elixir-phoenix` which holds Elixir, Phoenix and other needed tools for the application.

On the next few lines we're defining our working directory, copying files and making one new directory.

The `CMD` is the line which is executed when we fire up our container. There is two commands for fetching deps and firing up our server.

## docker-compose.yml

```yaml
version: '3.6'
services:
  db:
    environment:
      PGDATA: /var/lib/postgresql/data/pgdata
      POSTGRES_PASSWORD: postgres
      POSTGRES_USER: postgres
      POSTGRES_HOST_AUTH_METHOD: trust
    image: 'postgres:11-alpine'
    restart: always
    volumes:
      - 'pgdata:/var/lib/postgresql/data'
  web:
    build: .
    depends_on:
      - db
    environment:
      MIX_ENV: dev
    env_file:
      - .env
    ports:
      - '4000:4000'
    volumes:
      - .:/app
volumes:
  pgdata:
```
Here we defined our docker-compose script version, needed services and volumes to be created.

As you can see, there is two services(containers) being created `db` and `web`. `db` is our database container which is used by `web`-container. On the image-property we define our image, which is `postgres:11-alpine`. You could use newer version, for example PostgreSQL 12. It really depends what version you are going to use in production environment. I recommend to use same versions across the environments to minimize infrastructure related problems.

More explanation of the properties can be found from [docker-compose documentation](https://docs.docker.com/compose/compose-file/)

## .env

```.env
DATABASE_URL=postgres://postgres:postgres@db:5432/myapp_dev
```

For now there is only our environment variable for database URL. Later on when your application grows you will be storing your application environment variables in this file.

# Fire it up

First we need to build our Docker image:

```
$ docker-compose build
```

If you did everything correctly, the image should be built relatively quick. Now we have our image ready, but first we need to configure our application to use our environment variable for database URL, so lets do that.

Navigate to `config/dev.exs` and open it up. Change the content for your database from this:

```elixir
# Configure your database
config :myapp, Myapp.Repo,
  username: "postgres",
  password: "postgres",
  database: "myapp_dev",
  hostname: "localhost",
  show_sensitive_data_on_connection_error: true,
  pool_size: 10
```
To this:

```elixir
# Configure your database
config :myapp, Myapp.Repo,
  url: System.get_env("DATABASE_URL"),
  show_sensitive_data_on_connection_error: true,
  pool_size: 10
```

**NOTE: change endpoint IP to 0.0.0.0!**

As you can see, we are fetching our database URL for database connection from the container environment.

Now we should be ready for our application start-up:

```
$ docker-compose up
```
It checks the deps, compiles deps & source files, but wait a second...

```
[error] Postgrex.Protocol (#PID<0.3932.0>) failed to connect: ** (Postgrex.Error) FATAL 3D000 (invalid_catalog_name) database "myapp_dev" does not exist
```

We don't have a database where to connect! Lets fix that:

```
$ docker-compose run web mix ecto.create
```

After running this you should be greeted with a pleasant one line message which tells you that the database for YourApp.Repo has been created! Wonderful!

Note that you can execute any mix commands inside the container since it has mix tooling available. You can run migrations, seeds, destroy the database and set it up again for a clean start etc.

Now execute:

```
$ docker-compose up
```

The application should start up with database connection and be ready for development! Navigate to `localhost:4000` and now if you make changes to the source files, the changes will be updated to the server, so no need for manual restart of the containers!

# Conclusion

This is a pretty simplistic setup with minimal amount of files to support/configure.

I'll do a follow-up post where we will deploy Elixir application to AWS. It will include configuring the application for production environment, Terraform infrastructure and little bit of CI magic.

Thanks for reading, hope you liked it! 🙂