Overview

This post should be quick as I try to keep this setup easy and cheap. I'll start by going over the infrastructure and then finish it up by going through how I deploy sites.

Infrastructure

This site and all other projects you see hosted are running on a DigitalOcean droplet. The droplet SKU is the cheapest they have, the $4 droplet.

This VM comes with:

  • 1 vCPU
  • 512 MBs of RAM
  • 10 GBs of storage
  • 500 GBs of bandwidth

I do run into issues with the limited amount of RAM, but all those issues are related to deploying code and not the code that runs on them. I'll touch on that point later.

On this VM I run Caddy. It's a web server that I use to reverse proxy the sites. I would say it's slightly more easier to use than nginx.

The final component is Cloudflare, for better or for worse they have become the dominate tooling in this space. I use them for DNS routing because it is easy and free.

Below is how I configure the VM

VM setup process

Steps for setting up the server at https://jack-develops.com

Initial Environment Setup

Let's set up the environment first

mkdir sites/
apt update
apt upgrade --yes
apt install unzip git curl --yes
echo "export RELEASE_COOKIE=secret" >> /etc/profile
echo "export SECRET_KEY_BASE=secret" >> /etc/profile

Swap File

Due to the virtual machines limited amount of RAM, it will run out of memory when it comes to compiling things like erlang or even Elixir projects using Phoenix. By adding a swap file it can get through the compilation on these things. So what I do is have a swap file setup and turn it on when needed

fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile

This will create a file allocated for 1 gigabyte of storing, which on this VM means 9 GBs is left for everything else storage related. Then to turn it on and allow for the swaps to happen when under memory pressure

swapon /swapfile

This swap is now enabled until the machine reboots.

Install asdf

adsf is a package manager for installing many things. I use it for managing installed Elixir versions.

git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.1
echo ". $HOME/.asdf/asdf.sh" >> ~/.bashrc
source ~/.bashrc
asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git
asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
asdf plugin-add rebar https://github.com/Stratus3D/asdf-rebar.git

Install Erlang

Installing using adsf

apt -y install build-essential autoconf m4 libncurses-dev libwxgtk3.2-dev libwxgtk-webview3.2-dev libgl1-mesa-dev libglu1-mesa-dev libpng-dev libssh-dev unixodbc-dev xsltproc fop libxml2-utils openjdk-17-jdk
export KERL_CONFIGURE_OPTIONS="--disable-debug --without-javac"
asdf install erlang 26.2.5.3
asdf install rebar 3.24.0
asdf global rebar 3.24.0

Using asdf to install Erlang will fail due to error 137, running out of memory. The virtual machine has 500 megabytes of RAM which is not enough to build from source without using a swap. It's also worth noting that this will take a long time (15 minutes) to compile given the CPU speeds.

Thus installing from a debian package also works, here's how

apt install libncurses5 libsctp1 --yes
wget http://security.debian.org/debian-security/pool/updates/main/o/openssl/libssl1.1_1.1.1n-0+deb11u5_amd64.deb
dpkg -i libssl1.1_1.1.1n-0+deb11u5_amd64.deb
wget https://binaries2.erlang-solutions.com/debian/pool/contrib/e/esl-erlang/esl-erlang_26.2.3-1~debian~buster_amd64.deb
dpkg -i esl-erlang_26.2.3-1~debian~buster_amd64.deb

Install Elixir

Installing using adsf. Currently using a few versions

asdf local elixir 1.15
asdf local elixir 1.16

Caddy

Install Caddy, a web server that will serve our websites

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

Now we want to create the configuration file, Caddyfile

touch ~/Caddyfile

Then add the following content

jack-develops.com:80, www.jack-develops.com:80 {
  root * /var/lib/caddy/sour_shark/
  file_server
  encode gzip
  try_files {path}.html
  header ?Cache-Control "public, max-age=3600"
}

blue-mint.jack-develops.com:80 {
  reverse_proxy localhost:4001
}

wavy-bird.jack-develops.com:80 {
  respond "Not deployed"
}

icky-venus.jack-develops.com:80 {
  reverse_proxy localhost:4000
}

Finally reload the Caddyfile after making changes

caddy reload

Deploying Sites

I went through many ways to deploy these sites. My first route was to build a docker image and have the VM run those. Which I then quickly ran into the problem of different CPU architectures as I'm using an ARM (Apple silicon) CPU and the VM on DigitalOcean uses an Intel x86. My next strategy was to build the image on the VM instead, but this made for slow deployments, and overall, I found this to be overkill.

My next idea was to build the code locally and tarball it, then SCP it to the VM. But before I could do that I needed the VM setup to have the run times needed to run the code. This brought me to using adsf to install the run times needed. Tried using other tools but kept running out of memory during Erlang installs due to having to build from source.

But then I ran into the issue of the code built locally won't run on the VM due to CPU architecture differences. So I then went the route of having the code built on the VM.

Here's an example deploy_vm.sh script for an Elixir application

#!/bin/bash

rm /tmp/icky_venus.tar.gz
tar --exclude-from='deployment/exclude.txt' --no-xattrs -czvf /tmp/icky_venus.tar.gz . && \
scp /tmp/icky_venus.tar.gz root@digital-ocean:/tmp && \
ssh digital-ocean << EOF
rm -rf ~/sites/icky_venus/
mkdir -p ~/sites/icky_venus/
tar -xzvf /tmp/icky_venus.tar.gz -C ~/sites/icky_venus && \
rm /tmp/icky_venus.tar.gz && \
cd ~/sites/icky_venus/ && \
asdf local elixir 1.16 && \
asdf local erlang 26.2.5.3 && \
mix deps.get && \
MIX_ENV=prod mix release --overwrite && \
~/sites/icky_venus/_build/prod/rel/icky_venus/bin/icky_venus daemon && \
~/sites/icky_venus/_build/prod/rel/icky_venus/bin/icky_venus restart
EOF

Phoenix sites are a bit different

deployment/deploy_vm.sh
#!/bin/bash

rm /tmp/blue_mint.tar.gz
tar --exclude-from='deployment/exclude.txt' --no-xattrs -czvf /tmp/blue_mint.tar.gz . && \
scp /tmp/blue_mint.tar.gz root@digital-ocean:/tmp && \
ssh digital-ocean << EOF
rm -rf ~/sites/blue_mint/
mkdir -p ~/sites/blue_mint/
tar -xzvf /tmp/blue_mint.tar.gz -C ~/sites/blue_mint && \
rm /tmp/blue_mint.tar.gz && \
cd ~/sites/blue_mint/ && \
asdf local elixir 1.16 && \
asdf local erlang 26.2.5.3 && \
MIX_ENV=prod mix setup && \
MIX_ENV=prod mix assets.deploy && \
MIX_ENV=prod mix phx.gen.release && \
MIX_ENV=prod mix release && \
PHX_SERVER=true ~/sites/blue_mint/_build/prod/rel/blue_mint/bin/blue_mint daemon
EOF

Finally, if I need to IEx into a site and to tweak or inspect things I can do so by doing the following:

ssh digital-ocean
~/sites/icky_venus/_build/prod/rel/icky_venus/bin/icky_venus remote