Elixir 1.9 was released in June 2019 and with it came the exciting feature of releases, which allows for the compilation of your Phoenix application into a releases directory that you can run your application on through a executable command.
The purpose of this article is to deploy a Phoenix app to a server without relying on Docker containers, third-party hosting platforms (e.g. Heroku, Gigalixir). Prerequisites: You should be able to ssh into your host server with sudo access and be comfortable writing environment variables and navigating a Unix environment.
Prior to 1.9, Elixir apps relied on some additional programs to compile the app for deployment. Distillery would be used for the compilation of the application, and could be used in conjunction with edeliver to ssh into the target host server, run the build, download the build locally, and then deploy and run the app on the same server.
The documentation is pretty robust for bootstrapping a Phoenix app with Elixir 1.9. The deployment documentation also sets us up for releasing our app in staging and production environments. These steps are quite important and can allow for the deployment of not just our application but also:
- Handling environment variables at runtime
- Environment-specific configurations (
- Running database migrations every time we build a release
I'm not going to get into the details of writing our config files because these things are pretty well documented. When our configurations are all set up for production, we should be able to run the following and see the normal runtime server log and make sure the app is accessible at
MIX_ENV=prod mix release _build/prod/rel/my_app/bin/my_app start
Assuming this successfully runs locally, we can then move on to our remote host setup!
We'll want to perform the following steps on our remote server to make sure we can start pulling code from our git repo and build the executable:
- Install postgres if not installed and create a database for you application (if not stateless).
- Create a
adduser deploy) that handles both ssh connections to our local machine and our git repository. Add ssh keys to `/home/deploy/.ssh/authorized_keys'.
- Establish a directory that our build will live in, let's say
/home/deploy/myAppand pull down the latest code to that directory.
- Install Elixir 1.9 and Erlang on our remote server
wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo dpkg -i erlang-solutions_1.0_all.deb
mixto make sure Elixir is installed properly
- Navigate to our application directory, git pull our phoenix app, and build a release:
MIX_ENV=prod mix release
- Test the release out by running
_build/prod/rel/myApp/bin/myApp start. Is it running as normal? Can you navigate to
yourDomain:4000and see your app? If yes, great! If not, try to work out what is wrong with the configuration. Is the port open on your server? There could be a host (pun) of reasons why our server isn't serving our app correctly so that's outside of this tutorial.
Now that our application can successfully run, we need a way to make it run in the background!! The documentation assumes we can just run a Docker container which is suitable for a lot of use cases, particularly when it comes to scaling, or building clusters via Kubernetes, etc. However, it should be perfectly suitable to build and host a Phoenix app without that isolated environment. You shouldn't need Docker containers to run your app! So let's continue.
The next step I highly recommend for Phoenix deploys is writing a build script. The documentation briefly covers this through the perspective of a Docker file but we can break this down into a simple bash script that we can run whenever we want to run a release on our remote server.
#!/usr/bin/env bash # exit on error set -o errexit # Initial setup mix deps.get --only prod MIX_ENV=prod mix compile # Compile assets npm install --prefix ./assets npm run deploy --prefix ./assets mix phx.digest # Build the release and overwrite the existing release directory MIX_ENV=prod mix release --overwrite # Perform any migrations necessary _build/prod/rel/myApp/bin/myApp eval "MyApp.Release.migrate"
This is pretty straightforward. All we're doing is installing our mix dependencies, installing our npm packages, buiding our release, and running any migrations needed. Depending on our CI integration we can adopt this script to run a git pull beforehand too or adjust to however you want CI to be handled.
And this brings us to
systemd. While the history of systemd is somewhat controversial, it has been the de facto system and service manager for Linux distributions since 2014 and is super easy to set up and initialize our application with.
For the basic application I want to set up, it accomplishes three critical things:
- Runs the application in the background
- Auto initializes the application if the server reboots
- Provides logs for debugging and reference
I'm sure the use cases for a more complex configuration are out there, but my goal right now is limited to the above.
There actually is a mix library that allows you to generate a systemd unit file for your Phoenix application but it's simple enough that you can write one from scratch or copy and paste the following to
[Unit] Description=myApp service After=local-fs.target network.target [Service] Type=simple User=deploy Group=deploy WorkingDirectory=/home/deploy/build/myApp/_build/prod/rel/myApp ExecStart=/home/deploy/build/myApp/_build/prod/rel/myApp/bin/myApp start ExecStop=/home/deploy/build/myApp/_build/prod/rel/myApp/bin/myApp stop EnvironmentFile=/etc/default/myApp.env Environment=LANG=en_US.utf8 Environment=MIX_ENV=prod Environment=PORT=4000 LimitNOFILE=65535 UMask=0027 SyslogIdentifier=myApp Restart=always [Install] WantedBy=multi-user.target
We'll want to additionally create a
myApp.env file at
/etc/default so that our service can use runtime environment variables for our application. In my file I simply have the following:
PORT=4000 HOSTNAME="myApp.io" SECRET_KEY_BASE="[output of mix phx.gen.secret]" DATABASE_URL="ecto://postgres:password@myApp.io/[dbName]"
Now that our
systemd is configured, we can start our service. Here are some useful commands to play around with:
After making changes to the systemd unit files:
sudo systemctl daemon-reload
Start your service:
sudo systemctl start myapp.service
List systemd services:
systemctl list-units --type=service
Check the status of your service:
systemctl status myapp.service
This one is key because it will tell you if the service was able to start successfully. If it has, your app should be up and running! If you app isn't properly running we can check the logs via this command:
sudo journalctl -f -u myapp.service
sudo journalctl -u myapp.service --since today
Seems to be a very flexible logging service as well.[^1](https://www.digitalocean.com/community/tutorials/how-to-use-journalctl-to-view-and-manipulate-systemd-logs)
systemdscripts are executed normally at the root level, so the env vars need to be acccessible by root. Originally I had my
firstname.lastname@example.org vars set for the
deployuser but my
systemddidn't have access to them.
To set my env vars for runtime, I placed them in
/etc/default/dayoff.env, which included my
SECRET_KEY_BASE. I'm still a little bit uncertain if you need your vars accessible at the build directory by the
deployuser so you may additionally need to
exportthese vars at
Thanks for reading and I hope to get an article out next time around setting up nginx on the server (and maybe Nginx Ingress in Kubernetes).