Up and Running with Phoenix Live Reload and Docker

Up and Running with Phoenix Live Reload and Docker

It is becoming more and more common to deploy containerized applications for a variety of reasons:

  • consistency between development and production
  • isolation of the service environments
  • the ability to easily scale modular services

There are many more benefits of using containers, but you get the gist of it. The most commonly used container system is Docker. As it's popularity has risen, a growing number of people in the Phoenix community are deploying applications either inside or built within docker containers.

However, note the first benefit of containers that I have listed above. I develop on a Mac, but most often I deploy to Linux droplets on Digital Ocean. Even if I build my app inside Docker during the deployment process, then I am just putting off dealing with potential unseen problems while working on my Mac. This left me wanting to run Phoenix locally inside a Docker container.

You can certainly read the documentation for both Phoenix and Docker to create a setup from scratch, but there are many great articles on this topic. Here are a couple:

After taking my first crack at it, I had Phoenix up and running in a Docker container. The important elements of the setup are:

  • dockerfile: installs the base docker image & dependencies, sets up a directory for our codebase, and runs the shell script
  • docker compose file: defines the docker services that will be used (the phoenix image and the postgres image), provides environment variables, and the application's port
  • shell script file: installs dependencies (mix and npm), handles creating the database, and starts the phoenix server

Even with the application running fine, there was one issue. A major positive aspect of Phoenix development no longer worked: live reloading. With some minor changes I was able to get my development process back in shape.

dockerfile:

  • Add a line to install inotify tools. This is required for responding to file change events.
  • Remove the use of the Docker copy command. We will be using a different method for providing our codebase to the container. More on that shortly.
FROM elixir:1.8.2-alpine

RUN apk update
RUN apk upgrade --no-cache
RUN apk add nodejs=10.14.2-r0 nodejs-npm=10.14.2-r0
RUN apk add inotify-tools=3.20.1-r1
RUN apk add postgresql-client=11.3-r0
RUN mix local.rebar --force
RUN mix local.hex --force

RUN mkdir /app

WORKDIR /app

CMD ["sh", "./entrypoint.sh"]

docker-compose.yml:

version: '3.2'

services:
  phoenix:
    build:
      context: .
    volumes:
      - .:/app
    environment:
      PGUSER: postgres
      PGPASSWORD: postgres
      PGDATABASE: app
      PGPORT: 5432
      PGHOST: db
    ports:
      - "4000:4000"
    depends_on:
      - db
  db:
    image: postgres:9.6
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      PGDATA: /var/lib/postgresql/data/pgdata
    restart: always
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  pgdata:

webpack.config.js:

  • Add a configuration to tell webpack to poll for file changes.
const path = require('path');
const glob = require('glob');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = (env, options) => ({
  optimization: {
    minimizer: [
      new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: false }),
      new OptimizeCSSAssetsPlugin({})
    ]
  },
  entry: {
      './js/app.js': ['./js/app.js'].concat(glob.sync('./vendor/**/*.js'))
  },
  output: {
    filename: 'app.js',
    path: path.resolve(__dirname, '../priv/static/js')
  },
  watchOptions: {
    poll: true
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: '../css/app.css' }),
    new CopyWebpackPlugin([{ from: 'static/', to: '../' }])
  ]
});

After making those changes and restarting your container, you will notice that any asset or elixir file change will trigger a server/page reload. For a full example project check out this repo.

Subscribe for my latest posts and weekly news review