Deploying Phoenix App to Production Using Docker Compose

Ambrose MungaiAmbrose Mungai
4 min read

In this article, we’ll break down a real-world docker-compose.yml file used to deploy a Phoenix web application with Docker Compose, Traefik v2.9 as a reverse proxy, and environment-based configuration. This setup is designed for production with HTTPS, automated certificates via Let’s Encrypt, and health checks.

Instead of configuring the Phoenix application to terminate SSL connections, we use a proxy, Traefik, to handle all SSL connections. This enables us to use Let's Encrypt for TLS certificates. Using Traefik to manage this allows automatic renewal of certificates.

Health Check

To prepare the application for production deployment, we need to add a health check. The health check in your Docker Compose file ensures that the Phoenix application inside the main container is running and ready to receive traffic before other services (like Traefik) start routing requests to it.

The health check helps to:

  • Prevent routing to broken apps: If the health check fails, Traefik (and other dependent services) will not route traffic to the container.
  • Support zero-downtime deployments: It ensures the app is fully up before switching traffic, which is essential for rolling updates.
  • Improve observability: You can monitor container health in Docker dashboards or orchestration tools like Docker Swarm or Kubernetes.
defmodule MyappWeb.Healthcheck do
  @moduledoc """
  A plug for health check, bypasses TLS rewrites.
  """

  @behaviour Plug

  import Plug.Conn

  def init(opts), do: opts

  def call(%{request_path: "/health"} = conn, _) do
    conn
    |> send_resp(200, "")
    |> halt()
  end

  def call(conn, _), do: conn
end

Add the plug to the MyappWeb.Endpoint just after the socket definition:

  plug MyappWeb.Healthcheck

Mix Release

This article assumes you have configured your application as discussed in the previous blog post: Improving Your Elixir Configuration.

Elixir applications are prepared for deployment by running the MIX_ENV=prod mix release command. This command assembles a self-contained release that can be packaged and deployed, provided the target runs the same operating system distribution and version as the machine that ran the mix release command. The release directory includes the Erlang VM, Elixir, all code, and dependencies, which can then be deployed to the production machine.

Phoenix applications come with a Dockerfile to build Docker images. You can generate the Dockerfile by running:

mix phx.gen.release --docker

Environmental variables

To configure the deployed application we will use an .env file that will be in the same folder as the compose.yml.

Docker Compose File

This Compose file defines two primary services:

  • traefik: Acts as the HTTPS reverse proxy and certificate manager.
  • main: A Phoenix application container that listens on port defined in .env.

It also defines:

  • A shared volume for storing SSL certificate data.
  • A private network (myapp_network) connecting the services.

Traefik Service (Reverse Proxy)

services:
  traefik:
    image: traefik:v2.9

Traefik is configured as a standalone container that proxies HTTP(S) traffic to the Phoenix app.

Key configuration:

  • Networks: Connects to myapp_network to route traffic to the Phoenix app.
  • Volumes:
    • /var/run/docker.sock: Allows Traefik to discover running containers and their labels.
    • production_traefik:/etc/traefik/acme: Stores Let’s Encrypt TLS certificates.
  • Ports:
    • 80 for HTTP.
    • 443 for HTTPS.
  • Command Flags:
    • Sets up HTTP → HTTPS redirection.
    • Enables automatic TLS via Let’s Encrypt.
    • Uses Docker as a provider for service discovery.

Let's Encrypt

--certificatesResolvers.letsencrypt.acme.email=support@myapp.com
--certificatesResolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json

Traefik automatically generates and renews TLS certificates using the provided email and stores them in the mounted volume.

Phoenix App Service (main)

Build the Phoenix application using the Dockerfile in the current directory:

  main:
    container_name: main_phoenix_app
    build:
      context: .
      dockerfile: Dockerfile

Traefik Labels

These labels instruct Traefik how to route traffic to the Phoenix app:

  • Routing Rule: Matches both the root domain and www. version using the ${ENDPOINT_URL_HOST} environment variable.
  • Middlewares: Handle CSRF headers, redirect http://www.* to https://, and inject forwarded HTTPS headers.
  • TLS Settings: Enable HTTPS and link to the Let’s Encrypt resolver.

Compose File

volumes:
  production_traefik: {}

services:
  traefik:
    image: traefik:v2.9
    container_name: traefik
    networks:
      - myapp_network
    depends_on:
      - main
    volumes:
      - production_traefik:/etc/traefik/acme
      - /var/run/docker.sock:/var/run/docker.sock
    ports:
      - '0.0.0.0:80:80'
      - '0.0.0.0:443:443'
    restart: unless-stopped
    labels:
      - "application=traefik"
    command:
      - --log.level=INFO
      - --entryPoints.web.address=:80
      - --entryPoints.web.http.redirections.entryPoint.to=web-secure
      - --entryPoints.web.http.redirections.entryPoint.scheme=https
      - --entryPoints.web-secure.address=:443
      - --certificatesResolvers.letsencrypt.acme.email=support@myapp.com
      - --certificatesResolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json
      - --certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=web
      - --providers.docker=true
      - --providers.docker.exposedByDefault=false

  main:
    container_name: main_phoenix_app
    build:
      context: .
      dockerfile: Dockerfile
    env_file:
      - .env
    expose:
      - ${PORT}
    labels:
      - "application=main_phoenix_app"
      - "traefik.enable=true"
      - "traefik.http.middlewares.csrf.headers.hostsproxyheaders=X-CSRFToken"
      - "traefik.http.middlewares.redirect-https-www.redirectregex.regex=^https?://www\\.(.+)"
      - "traefik.http.middlewares.redirect-https-www.redirectregex.replacement=https://$${1}"
      - "traefik.http.middlewares.redirect-https-www.redirectregex.permanent=true"
      - "traefik.http.middlewares.forwarded-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
      - "traefik.http.routers.main.rule=(Host(`${ENDPOINT_URL_HOST}`) || Host(`www.${ENDPOINT_URL_HOST}`)) && PathPrefix(`/`)"
      - "traefik.http.routers.main.priority=1"
      - "traefik.http.routers.main.entryPoints=web-secure"
      - "traefik.http.routers.main.middlewares=redirect-https-www,csrf,forwarded-headers"
      - "traefik.http.routers.main.tls.certResolver=letsencrypt"
      - "traefik.http.routers.main.tls=true"
      - "traefik.http.services.main.loadBalancer.server.port=${PORT}"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:${PORT}/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    networks:
      - myapp_network
    restart: unless-stopped

networks:
  myapp_network:

Conclusion

In this article, we demonstrated how to deploy a Phoenix application to production using Docker Compose and Traefik as a reverse proxy. By offloading SSL termination to Traefik and automating certificate management with Let’s Encrypt, you simplify your deployment and ensure secure HTTPS connections. Adding a health check improves reliability and supports zero-downtime deployments. This setup provides a robust foundation for deploying Phoenix apps in production environments.

To see the code changes, check out this PR

0
Subscribe to my newsletter

Read articles from Ambrose Mungai directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ambrose Mungai
Ambrose Mungai