Background
I have a website built with Django and I would like to start writing parts of it in Phoenix.
Starting Point
In the last section we added nginx to our Dockerfile so that we could introduce Phoenix into our container and use nginx to split incoming requests between Phoenix & Django depending on the URL path.
Our next step is to get a basic Phoenix set up working and have it accessible via the Docker container. There are a few steps to this:
- Install Elixir & Phoenix
- Create a Phoenix project
- Add Elixir & Phoenix to our Docker image
- Add a nginx config section for our Phoenix app
- Get our Phoenix app running in the container
Install Elixir & Phoenix
In order to create a new Phoenix project and interact with it on our development machine, we need to install Elixir & Phoenix. We're not going to cover it in depth here but following the relevant install guides (Elixir & Phoenix) should get you set up.
Create a Phoenix Project
Phoenix comes with a mix task for creating a new Phoenix project. Mix is a build tool & task runner for the Elixir ecosystem. So common jobs like creating a project, adding modules or running tests often have mix tasks to help you do them.
In this case we're interested in mix phx.new
which creates a new project. We'd like the Phoenix project to live in a directory alongside the Django project code. Fortunately for me, the Django code is already in a dancetimetable
folder rather than directly in the project root, so we can create the Phoenix project in a folder called timetable
(naming is hard) in the project root as well.
In order to do that we're going to run the following command:
mix phx.new timetable --no-brunch
We use the --no-brunch
option because our Django project already uses Webpack for compiling the client side applications so we don't need Brunch.
The command asks us if we want to 'fetch and install dependencies' and we answer 'yes' because we do. Once it has finished installing & compiling, it prints the following helpful message:
We are all set! Go into your application by running:
$ cd timetable
Then configure your database in config/dev.exs and run:
$ mix ecto.create
Start your Phoenix app with:
$ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
$ iex -S mix phx.server
But we need to change these steps for our set up. The mix ecto.create
command is responsible for creating the Postgresql database for the Phoenix project but we want Phoenix to use the database that already exists for our Django app. So we're not going to run mix ecto.create
, instead we're going to edit config/dev.exs
to reference our development database. The change looks like this:
+config :timetable, Timetable.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
- database: "testing_dev",
- hostname: "localhost",
+ database: "tangotimetable",
+ hostname: System.get_env("DOCKER_HOST") || "localhost",
pool_size: 10
We prepare ourselves by adding the same DOCKER_HOST
lookup that we have discussed in a previous post for getting our Django server to talk to our Postgres database from inside the Docker container.
With that in place, we can run mix phx.server
. Once that is running we can visit http://localhost:4000
in our browser and see the default Phoenix new project landing page.
The rest of this post is about getting to see that page again, but having it served from inside the Docker container.
Add Elixir & Phoenix to our Docker image
To install Elixir & Phoenix into our Docker image, we use the marcelocg/phoenix Dockerfile as reference. After some experimentation, the start of our Dockerfile now looks like this:
from ubuntu:14.04
run apt-get update -y
run apt-get upgrade -y
# (1) Updated - Add locales & inotify-tools
run apt-get install -y git python python-pip make g++ wget curl libxml2-dev \
libxslt1-dev libjpeg-dev libenchant-dev libffi-dev supervisor \
libpq-dev python-dev realpath nginx apt-transport-https locales \
inotify-tools
# (2) Set locale information
run locale-gen en_US.UTF-8
env LANG en_US.UTF-8
env LANGUAGE en_US:en
env LC_ALL en_US.UTF-8
# Install yarn as before (unchanged)
run curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
# (3) Install erlang apt repository
run wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb \
&& dpkg -i erlang-solutions_1.0_all.deb
# (4) Update apt information & install erlang & elixir
run apt-get update -y
run apt-get install -y yarn esl-erlang elixir
# (5) Use mix to install Phoenix
env PHOENIX_VERSION 1.3.0
run mix archive.install --force https://github.com/phoenixframework/archives/raw/master/phx_new-$PHOENIX_VERSION.ez
# (6) Install hex & rebar locally
run mix local.hex --force \
&& mix local.rebar --force
# Same as before from here on down
add https://raw.githubusercontent.com/isaacs/nave/master/nave.sh ./nave.sh
run bash nave.sh usemain 6.9.1
# ...
Going through those changes one at a time:
- We add
inotify-tools
andlocales
to the install apt-get install. Phoenix usesinotify-tools
to check for file changes in Linux which allows it to reload the development server when the code has changed. As forlocales
, it seems that Erlang & Elixir like the Unix locale to be properly set before they are installed. I don't know why, but it complains if we don't do it. - We set the locale information as shown in the marcelocg/phoenix Dockerfile.
- Install the erlang-solutions apt repository package. This looks like we're installing a package, and we are, but the result of the package is to make our system aware of the erlang-solutions apt repository. This allows apt to install the latest packages from the erlang-solutions repository instead of the potentially-less-up-to-date Ubuntu repositories.
- Refresh apt's cache of information and install Erlang & Elixir. If you're unfamiliar with Erlang, it is the base environment upon with Elixir runs.
- Install Phoenix via the mix command. This installs Phoenix from the referenced archive file on Github. This is the recommended way to install Phoenix.
- We install hex & rebar locally. Hex is a package manager tool for Erlang & Elixir and rebar is a build tool. I do not know if these are necessary in our image, I haven't experimented with removing them yet.
Now we can rebuild our Docker image and it is ready to run our Phoenix development server. The rebuild may take some time due to the nature of the changes.
Add a nginx config section for our Phoenix app
The change to the nginx config is quite simple. We already have a section for the Django app. We just want another one for the Phoenix app. At the moment all requests go to the Django app. As an experimental step, we're going to redirect requests for /elixir
to our Phoenix app. This is never going to make it to production, but we're still getting set up.
So our change looks like this:
# server_name www.tangotimetable.com;
server_name localhost;
+ location /elixir {
+
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+ proxy_set_header Host $http_host;
+
+ # we don't want nginx trying to do something clever with
+ # redirects, we set the Host: header above already.
+ proxy_redirect off;
+
+ proxy_pass http://localhost:4000;
+ }
+
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
It is identical to the Django section below except that we're redirecting to port 4000 instead of port 7000. I am not sure, but I believe it is important for this section to appear above the Django section in the file. This means that the Django section acts as a catch-all for any requests not sent to the Phoenix app.
Get our Phoenix app running in the container
Now we're ready to get the Phoenix development server up and running in our container. We're going to follow a similar pattern to our Django app. We're going to add this section to our supervisord configuration:
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
+
+[program:phoenix]
+command=/src/dev/run-phoenix
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
Then we're going to add a run-phoenix
script which looks like this:
#!/bin/bash
cd /src/timetable
export DOCKER_HOST=`netstat -nr | grep '^0\.0\.0\.0' | awk '{print $2}'`
mix phx.server
See a previous post for discussion of the DOCKER_HOST
environment variable. It allows our Phoenix app to find the Postgres database which is on the host machine, not inside the container.
I leave it as an exercise to make sure that this run-phoenix
file is accounted for in the Dockerfile and makes it in the Docker image in the correct place. It can be very confusing when a file isn't included in the Docker image when you expect it to be.
The final change to make is to mount
the Phoenix app source folder into the Docker container at run time. This is because we haven't added the source code to the image. If we did, we'd have to rebuild the image on every change which would be painful.
So we're going to change our run-docker-dev
script, from the first post, like this:
-v `pwd`/logs/nginx:/var/log/nginx:rw \
-v `pwd`/logs/cron:/var/log/cron:rw \
-v `pwd`/dancetimetable:/src/dancetimetable:rw \
+ -v `pwd`/timetable:/src/timetable:rw \
-e DANCETIMETABLE_SETTINGS=development \
--rm \
--interactive \
Now when we run our Docker container and visit http://localhost:8000
we see the front page of our Django app but if we visit http://localhost:8000/elixir
we see the Phoenix new project landing page.
Update 2017-11-12: I forgot to include a necessary change to the Phoenix router. We have to change the timetable_web/router.ex
file with the following diff:
scope "/", TimetableWeb do
pipe_through :browser # Use the default browser stack
- get "/", PageController, :index
+ get "/elixir", PageController, :index
end
# Other scopes may use custom stacks.
So that Phoenix is expecting to be asked about /elixir
and not /
. We could instead change nginx to strip the /elixir
from the route before it reaches Phoenix but that is not the eventual goal I have for the routes that Phoenix should handle.
Conclusion
We now have our Django app & a new Phoenix app running side by side in the Docker container. We can make the Django app responsible for some URL paths and the Phoenix app responsible for others. We have a way to go before getting this into production but it is an important step!
If you have any questions or advice, please comment below.
Top comments (0)