Phoenix deploys with Elixir 1.9 with systemd (no Docker)
Tags: elixir, phoenix
Published: 2020-05-21 16:50:05

---
title: Phoenix deploys with Elixir 1.9 with systemd (no Docker)
published: true
description: Elixir 1.9 deploys with systemd
tags: Elixir, Phoenix
---

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](https://github.com/edeliver/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.

#### Testing our release locally

The documentation is pretty robust for [bootstrapping](https://hexdocs.pm/phoenix/up_and_running.html#content) a Phoenix app with Elixir 1.9. The [deployment](https://hexdocs.pm/phoenix/deployment.html) documentation also sets us up for [releasing](https://hexdocs.pm/phoenix/releases.html) 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 (`dev.exs` and `prod.exs`)
- 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 `localhost:4000`:

```bash
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!


#### Remote 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 `deploy` user (`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/myApp` and 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`
- Run `mix` to 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:4000` and 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.

#### Build script
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](https://hexdocs.pm/phoenix/releases.html#containers) 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.

<pre>
#!/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"
</pre>

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.

#### systemd

And this brings us to `systemd`. While the [history](https://www.linux.com/tutorials/understanding-and-using-systemd/) 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](https://github.com/cogini/mix_systemd) 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 `/etc/systemd/system/myapp.service`:

<pre>
[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
</pre>

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:

<pre>
PORT=4000
HOSTNAME="myApp.io"
SECRET_KEY_BASE="[output of mix phx.gen.secret]"
DATABASE_URL="ecto://postgres:password@myApp.io/[dbName]"
</pre>

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)


> #### Environment Variables

> `systemd` scripts are executed normally at the root level, so the env vars need to be acccessible by root. Originally I had my `deploy@dayoff.io` env vars set for the `deploy` user but my `systemd` didn't have access to them. 

> To set my env vars for runtime, I placed them in `/etc/default/dayoff.env`, which included my `PORT`, `DATABASE_URL`, and `SECRET_KEY_BASE`. I'm still a little bit uncertain if you need your vars accessible at the build directory by the `deploy` user so you may additionally need to `export` these vars at `~/.profile` or `~/.bash_profile`.

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).


## Useful Links


https://hexdocs.pm/phoenix/releases.html#containers

https://github.com/cogini/mix_systemd

https://github.com/cogini/mix-deploy-example

https://render.com/docs/deploy-phoenix

https://github.com/SolarisJapan/lunaris-wiki/wiki/Deploy-Phoenix-Project

https://github.com/phoenixframework/phoenix/blob/master/guides/deployment/releases.md#runtime-configuration

https://gist.github.com/davoclavo/61b9d84f2248f182c95ae7738490ddd1

https://hexdocs.pm/phoenix/releases.html#ecto-migrations-and-custom-commands

https://elixirforum.com/t/elixir-apps-as-systemd-services-info-wiki/2400

https://medium.com/@mcsonique/deploying-elixir-phoenix-projects-to-production-44a236c643c

https://www.digitalocean.com/community/tutorials/how-to-use-systemctl-to-manage-systemd-services-and-units