Flexible Dockerized Phoenix Deployments (1.2 & 1.3)

If I were to use HAProxy instead of nginx, would it simply be making a docker-compose file, like at Step 8, but configure a HAProxy Docker instance instead in the docker-compose.yml?

I already have an ansible playbook that deploys HAProxy and I’m in the middle of adding the deployment of letsencrypt on it. So would it be possible to skip Step 8 in this guide and separately install HAProxy with ansible? Would the backend still communicate with the ansible-deployed HAProxy?

I don’t really know much about Ansible but I would suspect that it either installs HAProxy to your local machine or to some kind of container/VM like Docker. Either way, you wouldn’t use the Docker network to connect your containers together since your HAProxy install wouldn’t be inside Docker. In that case, skip step 8 since you won’t need NGINX at all. Instead, check out the second note in step 6 about exposing the ports of your server container to the local host (the computer which is running Docker). Check out the Docker Compose docs here: https://docs.docker.com/compose/compose-file/#ports and use the ports: ... line in your compose file to expose the Phoenix server port to your local host. Then, you can use HAProxy to proxy port 80 traffic from the correct domain to your internal port that you exposed with Docker Compose (just like NGINX was doing).

I don’t know much about Ansible or HAProxy but I’m sure the above solution would work. Let me know if that makes sense :slight_smile:

OK thanks for the guidance. WIll chip away at this.

ok I’m at step 9. I skipped step 8 because I’m not using nginx(see above). I also removed all ‘nginx-network’ references because the exposure to the outside world will be managed by HAProxy. Anyway I do:

docker-compose up

and the first time it ran it build alpine linux + erlang but at the build admin phase there seems to be a file missing. Any suggestions? where is this path? it’s not on the build machine(my mac). when I do docker container ls I get zero items.

docker-compose.yml is after the log trace …

~/webapp/phx/kjvrvg (deploy-docker ✘)✹ ᐅ docker-compose up                 
Building admin
Step 1/5 : FROM bitwalker/alpine-erlang:20.2.2
 ---> ef93f28a7c3c
Step 2/5 : ENV MIX_ENV=prod
 ---> Using cache
 ---> 43ae9eab9504
Step 3/5 : ADD _build/prod/rel/kjvrvg/releases/0.0.1/kjvrvg.tar.gz ./
ERROR: Service 'admin' failed to build: ADD failed: stat /var/lib/docker/tmp/docker-builder271096858/_build/prod/rel/kjvrvg/releases/0.0.1/kjvrvg.tar.gz: no such file or directory

docker-compose.yml:

version: "3"
services:
  db:
    image: postgres:10.2-alpine
    container_name: kjvrvg-db
    environment:
      - POSTGRES_PASSWORD=postgres 
      - POSTGRES_DB=kjvrvg_prod
    # networks:
    #   - nginx-network
  admin:
    image: kjvrvg-release
    container_name: kjvrvg-admin
    build:
      context: .
      dockerfile: Dockerfile.run
    command: migrate
    # networks:
    #   - nginx-network
    depends_on:
      - db
  server:
    image: kjvrvg-release
    container_name: kjvrvg-server
    environment:
      - PORT=5000
      - HOST="maz.me"
    expose:
      - "5000"
    command: foreground
    # networks:
    #   - nginx-network
    depends_on:
      - db
      - admin
    ports:
      - "5000:5000"
# networks:
#   nginx-network:
#     external: true

Dockerfile.run:

FROM bitwalker/alpine-erlang:20.2.2

# Set environment variables
ENV MIX_ENV=prod

# Copy tarball release
ADD _build/prod/rel/kjvrvg/releases/0.0.1/kjvrvg.tar.gz ./

# Set user
USER default

# Set entrypoint
ENTRYPOINT ["./bin/kjvrvg"]

ADDITIONAL CONTEXT:

Here is the log of the original run. I noticed that the network created is “kjvrvgphx_default” which is not the name of the app (kjvrvg). Not sure if this could be the issue. After this run, I commented-out all references to ngnx-network in the docker-compose file. I’m still confused WHERE this missing file path is. There are no containers running or cached as far as I can tell.

~/webapp/phx/kjvrvg-phx (deploy-docker ✔) ᐅ docker-compose up
Creating network "kjvrvgphx_default" with the default driver
Pulling db (postgres:10.2-alpine)...
10.2-alpine: Pulling from library/postgres
ff3a5c916c92: Pull complete
a503b44e1ce0: Pull complete
211706713093: Pull complete
8df57d533e71: Pull complete
7858f71c02fb: Pull complete
55a8ef17ba59: Pull complete
3fb44f23d323: Pull complete
65cad41156b3: Pull complete
5492a5bead70: Pull complete
Digest: sha256:2cdf8430d7c1f59b3d3808f672944491aabf0fdf15da5b5e158e9fa4162453bf
Status: Downloaded newer image for postgres:10.2-alpine
Building admin
Step 1/5 : FROM bitwalker/alpine-erlang:20.2.2
20.2.2: Pulling from bitwalker/alpine-erlang
2fdfe1cd78c2: Pull complete
dca9997744b6: Pull complete
f36bae45af9b: Pull complete
Digest: sha256:bbd15b4728797e5d95aef467b456737782706f1a52951ac360643620da789771
Status: Downloaded newer image for bitwalker/alpine-erlang:20.2.2
 ---> ef93f28a7c3c
Step 2/5 : ENV MIX_ENV=prod
 ---> Running in bac96ef5e2d3
Removing intermediate container bac96ef5e2d3
 ---> 43ae9eab9504
Step 3/5 : ADD _build/prod/rel/kjvrvg/releases/0.0.1/kjvrvg.tar.gz ./
ERROR: Service 'admin' failed to build: ADD failed: stat /var/lib/docker/tmp/docker-builder980064711/_build/prod/rel/kjvrvg/releases/0.0.1/kjvrvg.tar.gz: no such file or directory

also:

~/webapp/phx/kjvrvg (deploy-docker ✘)✹ ᐅ docker rmi kjvrvg-build kjvrvg-release
Error: No such image: kjvrvg-build
Error: No such image: kjvrvg-release

build.sh

#!/bin/sh

# Remove old releases
rm -rf _build/prod/rel/*

# Build the image
docker build --rm -t kjvrvg-build -f Dockerfile.build .

# Run the container
docker run -it --rm --name kjvrvg-build -v $(pwd)/_build/prod/rel:/opt/app/_build/prod/rel kjvrvg-build

You are deleting your builds in the build.sh, you need to rebuild your release after that.

The name of the network is automatically created from the name of the folder you are in with docker-compose and unrelated to your problem. That name is usually not important.

I never called build.sh. Sorry, probably shouldn’t have appended that.

You need to call build.sh though. The system works in two stages:

  1. The first stage is building the release tarball, which requires you call build.sh, which uses Dockerfile.build to build your release tarball in _build with Distillery
  2. Only after you have done that can you call docker-compose up to actually run the app using Dockerfile.run.

Let me know if that works!

Yes, that helped thanks and I got the release built … with webpack too!

So now I have a release but no network so does that still mean that kjvrvg-server got deployed like the section below? even though I commented-out nginx-network? All I have to do is link up my HAProxy?

  server:
    image: kjvrvg-release
    container_name: kjvrvg-server
    environment:
      - PORT=5000
      - HOST="maz.me"

One of the things with HAProxy is that it seems to expect an IP address for the internal node:

server server1 127.0.0.1:8000 maxconn 32

but the proxy forwarding with nginx does not necessarily need one. Not sure how to get around this.

This is awesome! Thanks for the useful guide and all related comments everyone. :smiley:

@jswny, I found myself using Swarm cluster with apps running as services. You can have Swarm cluster with 1 machine.
In Swarm this configuration will allow you to restart your application container without nginx or start nginx before your applications:

  resolver 127.0.0.11 ipv6=off valid=30s;

  set $upstream app_umbrella:80;

  location / {
    proxy_pass http://$upstream;
  }

It helps when you need to update your service with new version and do not want to restart nginx service.

That’s pretty cool, I’ll have to check out Swarm. I admit it has been on my radar but I haven’t had time to look into it yet :slight_smile: You can actually leave NGINX running when you restart you services, but it might spam your logs with service unavailable stuff.

Yeah for the IP address I’m sure you can just use localhost or 127.0.0.1 and then the port like you have since the IP is just your local machine and the correct port.

The point of the nginx-network is not even specific to NGINX itself, it’s actually just to have a Docker network setup so that the services can communicate with each other. I just named it nginx-network since instead of being specific to a certain app that you have deployed, it is the overall network for all of your apps since NGINX needs to communicate with all of them, so you have to have all of the apps on the same network. I think as long as your database container and app container are on the same Docker network it won’t matter what the network is called. The app container just needs to be able to communicate with the database container. Then, like I said you expose the app container’s server port to the outside world (your local machine) for HAProxy to send traffic to.

Let me know if that makes sense!

so after ./build.sh and docker-compose up I open http://localhost:5000 in a browser

and in the log I get
kjvrvg-server | ** (UndefinedFunctionError) function Mix.env/0 is undefined (module Mix is not available)

so I need to tip-off phoenix that I’m running on prod so I do:

config.exs:

config :kjvrvg,
  :environment, Mix.env()

but this is no change. What do I need to do to ensure that phoenix knows that I’m in a prod env and I should not use Mix?

also did dev.exs:

config :kjvrvg,
:environment, :dev

prod.exs:

config :kjvrvg,
:environment, :prod

kjvrvg-server | 03:30:14.049 [info] GET /
kjvrvg-server | 03:30:14.050 [info] Sent 500 in 878µs
kjvrvg-server | 03:30:14.054 [error] #PID<0.1460.0> running KjvrvgWeb.Endpoint terminated
kjvrvg-server | Server: localhost:5000 (http)
kjvrvg-server | Request: GET /
kjvrvg-server | ** (exit) an exception was raised:
kjvrvg-server |     ** (UndefinedFunctionError) function Mix.env/0 is undefined (module Mix is not available)
kjvrvg-server |         Mix.env()
kjvrvg-server |         (kjvrvg) lib/kjvrvg_web/views/layout_view.ex:13: KjvrvgWeb.LayoutView.webpack_css_path/2
kjvrvg-server |         (kjvrvg) lib/kjvrvg_web/templates/layout/app.html.eex:11: KjvrvgWeb.LayoutView."app.html"/1
kjvrvg-server |         (phoenix) lib/phoenix/view.ex:332: Phoenix.View.render_to_iodata/3
kjvrvg-server |         (phoenix) lib/phoenix/controller.ex:740: Phoenix.Controller.do_render/4
kjvrvg-server |         (kjvrvg) lib/kjvrvg_web/controllers/page_controller.ex:1: KjvrvgWeb.PageController.action/2
kjvrvg-server |         (kjvrvg) lib/kjvrvg_web/controllers/page_controller.ex:1: KjvrvgWeb.PageController.phoenix_controller_pipeline/2
kjvrvg-server |         (kjvrvg) lib/kjvrvg_web/endpoint.ex:1: KjvrvgWeb.Endpoint.instrument/4

What Elixir code does LayoutView.webpack_css_path/2 contain? The first couple of webpack/Phoenix guides I found seem to assume that the Mix module is available to conditionally link to hot-reloaded CSS/JS, but, if I understand correctly, this won’t be the case if the app is running as a release built by Distillery, as I believe you’re finding.

If that’s the case, can you simplify LayoutView.webpack_css_path/2 so that it always outputs ‘/css/styles.css’ or whatever, without using Mix? (And the same for the JS function.)

I haven’t used webpack myself yet, and it’s entirely possible my post is misguided/wrong. If anyone has a link to a guide on replacing brunch with the latest version of webpack in Phoenix that works with releases, that would be great. :slight_smile:

1 Like

Like @BrightEyesDavid said, you can’t really use Mix in prod since it isn’t included in the release. So, what you should do is have Webpack build to the priv directory in your app’s root directory (like how the default Brunch build process works). Then, Distillery will package up your priv directory with your release, which you can then access in your code with priv_dir = Application.app_dir(:myapp, "priv"). You should probably then try rewriting that LayoutView.webpack_css_path/2 to pull from the built files in the priv directory.

This is what it contains:

  def webpack_js_path(conn, path) do
    if Mix.env == :dev do
      static_path(conn, path)
    else
      "//localhost:8080#{path}"
    end
  end
  
  def webpack_css_path(conn, path) do
    if Mix.env == :dev do
      static_path(conn, path)
    else
      ""
    end
  end

app.html.eex:
<link rel="stylesheet" href="<%= webpack_css_path(@conn, "/css/app.css") %>">
<script src="<%= webpack_js_path(@conn, "/js/app.js") %>"></script>

so this is a little busted I guess, I will remove the Mix.env references, the whole if-structure and just unconditionally do:
"//localhost:8080/css/app.css"
and
"//localhost:8080/js/app.js"

Michael

~~ UPDATE ~~

So I did the above and phoenix is up and running(!), not getting the error anymore, but I think I broke css/webpack since the page is rendering completely unstyled. would it be the //localhost:8080/css/app.css path syntax? I feel this is a rigid hack.
here is what layout_view.ex looks like now:

defmodule KjvrvgWeb.LayoutView do
  use KjvrvgWeb, :view

  def webpack_js_path(conn, path) do
    "//localhost:8080/js/app.js"
  end
  
  def webpack_css_path(conn, path) do
    "//localhost:8080//css/app.css"
  end

end

kjvrvg-server | 04:06:57.909 [info] GET /
kjvrvg-server | 04:06:57.909 [info] Sent 200 in 356µs

1 Like

Mix.env might be not available during runtime. But you can use it during compilation

if Mix.env == :dev do
  def webpack_js_path(conn, path) do
    static_path(conn, path)
  end
else
  def webpack_js_path(conn, path) do
    "//localhost:8080#{path}"
  end
end

Ah, that’s good to know.

I think the paths should be the other way around. (Static path for production.)

The site now runs, but I’m not getting any styles using hard coded paths. I’m not sure if it’s malformed paths, missing resources or if webpack-server failed to run.

defmodule KjvrvgWeb.LayoutView do

use KjvrvgWeb, :view

def webpack_js_path(conn, path) do
//localhost:8080/js/app.js
end

def webpack_css_path(conn, path) do
//localhost:8080//css/app.css
end

end

I’m not sure if I fully understand your post, but if this is for a release in production, I don’t think you want localhost:8080. My understanding is that that’s the webpack dev server, for hot reloading of assets during development.

Based on @idi527’s reply above, try:

if Mix.env == :prod do
  def webpack_js_path(conn, path) do
    static_path(conn, path)
  end
else
  def webpack_js_path(conn, path) do
    "//localhost:8080#{path}"
  end
end
if Mix.env == :prod do
  def webpack_css_path(conn, path) do
    static_path(conn, path)
  end
else
  def webpack_css_path(conn, path) do
    "//localhost:8080#{path}"
  end
end

I think you’ll also need to ensure that webpack is outputting the assets to whatever path static_path(conn, path) gives, if it doesn’t already.

1 Like