Optimizing for Free Hosting — Elixir Deployments

Kubernetes (kidding) — Photo credit Jp Valery on Unsplash

Another way to deploy Elixir apps

I’m going to walk through a deployment strategy that can take you start-to-finish with a full-featured Phoenix web app running on a free-forever Google Cloud Platform Compute Engine instance.

What we’re optimizing for

Free without the compromises (except your time of course)

Overview

The Details!

1 — Get a domain

2 — Provision an F1-Micro Compute Engine instance

Name: whatever you want
Region: double check https://cloud.google.com/free to make sure the region you want is listed in the Compute Engine section
Series: N1
Machine Type:
f1-micro
Boot Disk:
Operating System: Ubuntu
Version: Ubuntu 20.04 LTS
Boot Disk Type: Standard persistent disk, 30GB
Firewall: allow both HTTP and HTTPS traffic

3 — Make the IP address of the instance static

4 — Add your SSH key

5 — Set up SSH alias

Host my_app
Hostname <IP address from step 3>
User <your local user name>
Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_rsa

6 — Enable swap memory on the instance

sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo ‘/swapfile none swap sw 0 0’ | sudo tee -a /etc/fstab

7 — Install Erlang, Elixir, Node, and Postgres

wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb && sudo dpkg -i erlang-solutions_2.0_all.debsudo apt updatesudo apt install esl-erlangsudo apt install elixir
sudo apt install nodejs npm
sudo apt install postgresql postgresql-contrib

8 — Set a secure password for Postgres

sudo -u postgres psql
\password postgres(paste the password you generated and hit enter)(now put \q and hit enter to quit)

9 — Configure Postgres to allow remote connections

Name: whatever you want, something like "database"
Targets:
All instances in the network
Source IP Ranges: 0.0.0.0/0
tcp: Check box, and enter 5432 in field
sudo find / -name "postgresql.conf"
sudo vim <the file path you found>
sudo vim <the file path you found>
host    all             all             0.0.0.0/0               md5
host all all ::/0 md5

10 — Connect to your remote Git repo where your project lives

git clone https://github.com/<YOUR_GITHUB_USERNAME/optimized_nginx

11 — Get your app secrets onto the server

scp config/prod.secret.exs my_app:~/my_app/config/
MIX_ENV=prod mix deps.getMIX_ENV=prod mix ecto.create

12 — Install Nginx + Certbot for SSL

sudo apt install nginx
sudo vim /etc/nginx/sites-available/default
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d <your_domain>.com -d www.<your_domain>.com
sudo certbot renew --dry-run

13 — Configure Nginx to reverse proxy requests to your app

sudo vim /etc/nginx/sites-available/default
upstream phoenix {
server 127.0.0.1:4000;
}
if ($host = www.<your_domain>.com) {
return 301 $scheme://<your_domain>.com$request_uri;
}
location /.well-known {
alias /var/www/.well-known;
}
location / {
allow all;

# Proxy Headers
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Cluster-Client-Ip $remote_addr;

# WebSockets
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Proxies to whatever you set in the 'upstream phoenix {...' block
proxy_pass http://phoenix;
}
sudo systemctl reload nginx
# get the assets ready
cd assets && npm install && cd ../
npm run deploy --prefix ./assets
MIX_ENV=prod mix phx.digest
# run the app on port 4000
PORT=4000 MIX_ENV=prod mix phx.server

14 — Build a deployment shell script

vim deploy.sh
#!/bin/bash
set -e
# Update to latest version of code
cd /home/<YOUR_USERNAME>/my_app
git fetch
git reset --hard origin/master
MIX_ENV=prod mix deps.get
# Optional CI steps
mix test
# mix credo --strict (credo is not in example repo, commented out)
if System.get_env("CI") do
import_config "test.secret.exs"
end
# Build phase
export MIX_ENV=prod
mix compile
npm install --prefix ./assets
npm run deploy --prefix ./assets
mix phx.digest
# Create release
now_in_unix_seconds=$(date +'%s')
mix release --path ../releases/${now_in_unix_seconds}
# Update env var file with latest release name
sed -i 's/LATEST_RELEASE=.*/LATEST_RELEASE='$now_in_unix_seconds'/g' ../env_vars
# Find the port in use, and the available port
if $(curl --output /dev/null --silent --head --fail localhost:4000)
then
port_in_use=4000
open_port=4001
else
port_in_use=4001
open_port=4000
fi
# Put env vars with new port and set non-conflicting node name
echo "export PORT=${open_port}" >> ../releases/${now_in_unix_seconds}/releases/0.1.0/env.sh
echo "export RELEASE_NAME=${open_port}" >> ../releases/${now_in_unix_seconds}/releases/0.1.0/env.sh
# Start app on open port
sudo systemctl start my_app@${open_port}
# Pause script till app is fully up
until $(curl --output /dev/null --silent --head --fail localhost:$open_port); do
printf 'Waiting for app to boot...\n'
sleep 1
done
# Run migrations
mix ecto.migrate
# Update Nginx config to direct requests to app on open port
sudo sed -i 's/server 127\.0\.0\.1\:.*/server 127.0.0.1:'$open_port\;'/g' /etc/nginx/sites-available/default
# Reload Nginx so it gracefully starts routing to new app version
sudo systemctl reload nginx
# Stop previous version of app
sudo systemctl stop my_app@${port_in_use}
chmod u+x deploy.sh
LATEST_RELEASE=

15 — Create systemd services

sudo vim /etc/systemd/system/my_app@.service
sudo systemctl enable my_app_start_on_boot

16 — Deploy the app

ssh my_app ./deploy.sh

To really see the magic, commit and push a change locally and then run the command again, and you’ll see it gracefully swap over to the new version without any downtime 🎉🎉🎉.

17 — Attach to journald to watch logs, and configure to trim

ssh my_app journalctl -f
ssh my_app journalctl -n 500
ssh my_app journalctl -n 50 -u my_app@4001
Nov 03 21:43:33 instance-1 systemd[1]: Stopped My App.
SystemMaxUse=4G
sudo systemctl restart systemd-journald.service

You made it! 🙌

Bonus 1: secure your data against the possibility of eventual hardware failure by creating another Google account with a free instance and set up automated backups using cron

sudo apt install postgresql postgresql-contrib
#hostname:port:database:username:password
<server IP address>:5432:my_app:postgres:<postgres password>
pg_dump -U postgres -h <server IP address> -p 5432 my_app > ~/my_app/my_app_hourly.bak

Bonus 2: Rollback script

chmod u+x rollback.sh

Elixir dev building for the web with Phoenix

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store