← articles

Web Application Deployment on a Budget

Nate Goldman June 21, 2019

How to set up your own Platform-as-a-Service server from scratch

Hello gentle reader! If you have landed here, chances are you want to set up a server to deploy your own web applications with a minimum of effort and expenditure.

The idea here is to take a budget Ubuntu VPS ($5 from most providers) and have your own git-deployment platform ready to go within an hour or two.

Create a new server

First you need a server running latest Ubuntu LTS and a domain pointed at that server’s IP.

For the purposes of this walkthrough I’m using namecheap as a domain registrar and digitalocean to host a VPS as I’ve found them to be both affordable and reliable (still using them 3 years later, which is a lot in internet years).

I’ve gone ahead and created a new VPS with a Ubuntu 16.04 x64 image, a 512 MB CPU, and a 20 GB SSD HD (your basic $5 digital ocean box). I’ve also checked the free monitoring option and added my SSH keys via the web interface.

Set up DNS

For this walkthrough I’m using a domain I bought named decent.digital.

For DNS configuration, I recommend doing the following three things:

If you’re working with namecheap, these settings can be found in the “Advanced DNS” tab on the page of the domain you’re working.

Create a new user and disable root

Assuming you’ve gotten this far, it’s worth taking a some time to make our server a little more secure before jumping into anything else.

In this section we’re going to:

  1. Make sure all packages are up to date
  2. Create a new user
  3. Add SSH keys for the new user
  4. Disable password authentication
  5. Test that everything works
  6. Disable root login

1. Make sure all packages are up to date

First and foremost, let’s make sure our system is completely up to date.

$ ssh root@decent.digital
root@decent:~$ apt-get update
root@decent:~$ apt-get upgrade
root@decent:~$ apt-get autoremove

2. Create a new user

Now we’re going to create a new user. This is a security precaution to reduce the likelihood of your VPS getting taken over. I’ll create a new user for myself called ng.

# copy your public key
$ cat ~/.ssh/id_rsa.pub | pbcopy

# connect to your server
$ ssh root@decent.digital

# create a new user
root@decent:~$ adduser ng
root@decent:~$ usermod -aG sudo ng

3. Add SSH keys for the new user

Now we need to set up ssh keys for this new user so we can login remotely without needing to enter a password every time.

# switch to new user
root@decent:~$ su - ng

# create .ssh directory
ng@decent:~$ mkdir ~/.ssh
ng@decent:~$ chmod 700 ~/.ssh

# open nano editor to create authorized_keys file
ng@decent:~$ nano ~/.ssh/authorized_keys

# paste your public key, exit nano (ctrl+x) and write the new file (prompt)
ng@decent:~$ chmod 600 ~/.ssh/authorized_keys

# return to being root user
ng@decent:~$ exit

4. Disable password authentication

I recommend disabling password logins altogether to make your server as secure as possible. To do this, edit /etc/ssh/sshd_config to ensure PasswordAuthentication is set to no.

# edit sshd config
root@decent:~$ sudo nano /etc/ssh/sshd_config
# => PasswordAuthentication no
# => PubkeyAuthentication yes
# => ChallengeResponseAuthentication no

# reload ssh daemon
root@decent:~$ sudo systemctl reload sshd

5. Test that everything works

In another terminal, test ssh works with new user.

$ ssh ng@decent.digital
# => Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-98-generic x86_64)

6. Disable root login

If that worked, it should now be safe to disable the root user in sshd_config (PermitRootLogin no) and exit.

# edit sshd config
root@decent:~$ sudo nano /etc/ssh/sshd_config
# => PermitRootLogin no

# # reload ssh daemon
root@decent:~$ sudo systemctl reload sshd

Setting up git deployments with dokku

Dokku is a nifty little program that helps you set up your server to deploy and host sites using git. It’s modeled after platform as a service providers like heroku, and built on top of docker. It’s fairly low stress and has worked fine for me as a low-cost self-hosted platform for both testing applications under development and serving production websites for a couple of years now.

In this section we’ll be doing the following:

  1. Installing and configuring dokku
  2. Setting up a basic HTML site
  3. Deploying a basic HTML site
  4. Adding SSL with Let’s Encrypt
  5. Adding a default app for subdomains

1. Installing and configuring dokku

# for debian systems, installs dokku via apt-get
$ wget https://raw.githubusercontent.com/dokku/dokku/v0.10.5/bootstrap.sh
$ sudo DOKKU_TAG=v0.10.5 bash bootstrap.sh

# go to your server's domain or IP in the browser and follow the web installer instructions
$ open decent.digital

For the web installer portion, all you have to do is specify the hostname (decent.digital) and check the box for using virtual hostnames. That’s it! Your server should be ready to deploy a new app.

2. Setting up a basic HTML site

Now we’re going to deploy the most basic website possible: a good old html page.

# create a new folder
~ $ mkdir decent && cd decent

# initialize a new git repository
~/decent $ git init

# create www folder & create www/index.html
~/decent $ mkdir www
~/decent $ touch www/index.html

# edit index.html
~/decent $ $EDITOR www/index.html

Here we’ll just make a basic html page to make sure the deployment is working as expected.

<!DOCTYPE html>
<html lang="en">
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>D E C E N T</title>
  <h1>D E C E N T</h1>

Now we need to create a file to tell dokku what kind of deployment this is. I’m going to use a barebones nginx buildpack. For the purposes of this walkthrough all we need is an empty .static file at the root of the repo (see https://github.com/dokku/buildpack-nginx for detailed instructions).

~/decent $ touch .static

Now we should commit the files we created.

~/decent $ git add www/index.html .static
~/decent $ git commit -m '.'

This should be enough to get us started.

3. Deploying a basic HTML site

Now we need to add the right git remote address for our app.

~/decent $ git remote add deploy dokku@decent.digital:decent.digital

You can break down the remote address by splitting it at the colon (:) and reading it as follows:

The first part (dokku@decent.digital) is the address of the server.

The second part (decent.digital) is the address of the repo (or app).

If the app has its own domain, this should be the full hostname. Note that you can point DNS at this server’s IP address, then host the app for any domain this way.

If you want the app to be on the subdomain of the main domain (<subdomain>.decent.digital in this case), the second part should just be the text of the subdomain. So for example if I want to deploy an app to test.decent.digital, the remote address would be dokku@decent.digital:test.

Once your remote is properly set up, all you need to do is push.

~/decent $ git push deploy master

You’ll now get some live info on the deployment process courtesy of the buildpack and some magic from herokuish.

Once it’s done you should be able to visit your site on the internet.

4. Adding SSL with Let’s Encrypt

It’s a good idea to get SSL encryption set up right away so you don’t have to think about it again. Dokku and Let’s Encrypt play nice together thanks to the dokku-letsencrypt plugin.

# install the plugin
ng@decent:~$ sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git

# set up email address for SSL certs
ng@decent:~$ dokku config:set --no-restart decent.digital DOKKU_LETSENCRYPT_EMAIL=<e-mail>

# add encryption
ng@decent:~$ dokku letsencrypt decent.digital

You should now get redirected to https when you visit your site.

5. Adding a default app for subdomains

Now we should set up a “default” app for when someone tries to visit a subdomain with no app.

All we need to do is create an app whose name will be “first” lexically – I tend to use the name 00-default.

$ mkdir -p 00-default && cd 00-default
~/00-default $ touch .static
~/00-default $ echo "no app found" > index.html
~/00-default $ git init
~/00-default $ git add .
~/00-default $ git commit -m '.'
~/00-default $ git remote add deploy dokku@decent.digital:00-default
~/00-default $ git push deploy master

Note that let’s encrypt can’t do wildcard certification for subdomains so https won’t work for this one.


Adding a swapfile

On a cheap machine ($5 on DO), you can run into memory issues because of infrequent but somewhat costly deployment tasks (npm install is a major culprit). Other than these memory-hogging install scripts, the memory footprint of dokku and the apps deployed on it is fairly small. Adding a swap file is a good way to mitigate this issue and save a little money.

ng@decent:~$ sudo swapon --show
# no output means no swapfile is set up

ng@decent:~$ sudo fallocate -l 1G /swapfile
ng@decent:~$ ls -lh /swapfile
# -rw-r--r-- 1 root root 1.0G Nov 12 11:01 /swapfile
ng@decent:~$ sudo chmod 600 /swapfile
ng@decent:~$ ls -lh /swapfile
# -rw------- 1 root root 1.0G Nov 12 11:01 /swapfile
ng@decent:~$ sudo mkswap /swapfile
# Setting up swapspace version 1, size = 1024 MiB (1073737728 bytes)
# no label, UUID=265742ea-aacc-4cc4-9ac5-51df2d31f2c7
ng@decent:~$ sudo swapon /swapfile
ng@decent:~$ sudo swapon --show
# /swapfile file 1024M   0B   -1
# ng@decent:~$ free -h
#               total        used        free      shared  buff/cache   available
# Mem:           488M         91M         44M        3.4M        352M        362M
# Swap:          1.0G          0B        1.0G
ng@decent:~$ sudo cp /etc/fstab /etc/fstab.bak
ng@decent:~$ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# /swapfile none swap sw 0 0
ng@decent:~$ cat /proc/sys/vm/swappiness
# 60
ng@decent:~$ sudo sysctl vm.swappiness=10
# vm.swappiness = 10
ng@decent:~$ cat /proc/sys/vm/vfs_cache_pressure
# 100
ng@decent:~$ sudo sysctl vm.vfs_cache_pressure=50
# vm.vfs_cache_pressure = 50
ng@decent:~$ cat /proc/sys/vm/vfs_cache_pressure
# 50
ng@decent:~$ sudo nano /etc/sysctl.conf
# add the following lines to the bottom of the above file:
# vm.swappiness = 10
# vm.vfs_cache_pressure = 50

For a full walkthrough, see: https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-16-04

Adding a custom MOTD

I like to have a status update on apps right away when I log in, and it’s nice to get a little report on system status too (CPU, memory, disk). An easy way to set this up is to customize the MOTD (message of the day), which is what you see every time you log in.

On Ubuntu 16.04, the scripts that define the MOTD live in /etc/update-motd.d/. They’re just a collection of scripts run in order that write to the terminal when you log in.

ng@decent:~$ cd /etc/update-motd.d/
ng@decent:/etc/update-motd.d$ ls -la
total 36
drwxr-xr-x  2 root root 4096 Nov 12 11:52 .
drwxr-xr-x 97 root root 4096 Nov 12 11:04 ..
-rwxr-xr-x  1 root root 1642 Nov 12 11:52 00-header
-rwxr-xr-x  1 root root   97 May 24  2016 90-updates-available
-rwxr-xr-x  1 root root  299 Jul 22  2016 91-release-upgrade
-rwxr-xr-x  1 root root  111 May 11  2017 97-overlayroot
-rwxr-xr-x  1 root root  142 May 24  2016 98-fsck-at-reboot
-rwxr-xr-x  1 root root  144 May 24  2016 98-reboot-required
-rwxr-xr-x  1 root root  346 Nov 12 10:41 99-dokku

In my case I’ve deleted a couple of junk files from ubuntu (ad for cloud customer support, links to help sites), and replaced the 00-header file with the following:

#    00-header - fancy MOTD header

[ -r /etc/lsb-release ] && . /etc/lsb-release

if [ -z "$DISTRIB_DESCRIPTION" ] && [ -x /usr/bin/lsb_release ]; then
  # Fall back to using the very slow lsb_release utility
  DISTRIB_DESCRIPTION=$(lsb_release -s -d)

UPTIME_DAYS=$(expr `cat /proc/uptime | cut -d '.' -f1` % 31556926 / 86400)
UPTIME_HOURS=$(expr `cat /proc/uptime | cut -d '.' -f1` % 31556926 % 86400 / 3600)
UPTIME_MINUTES=$(expr `cat /proc/uptime | cut -d '.' -f1` % 31556926 % 86400 % 3600 / 60)

R=`echo "\033[1;31m"`
G=`echo "\033[1;32m"`
N=`echo "\033[0m"`

cat << EOF
<Insert Cool ASCII Art Here>

${N}ip:           ${G}`ifconfig eth0 | grep "inet addr" | awk -F: '{print $2}' | awk '{print $1}'`
${N}uptime:       ${G}$UPTIME_DAYS days, $UPTIME_HOURS hours, $UPTIME_MINUTES minutes

${N}cpu:         ${G}`cat /proc/cpuinfo | grep 'model name' | head -1 | cut -d':' -f2`
${N}os:           ${G}$DISTRIB_DESCRIPTION `uname -o` `uname -r` `uname -m`

${N}cpu load:     ${G}`cat /proc/loadavg | awk '{print $1 ", " $2 ", " $3}'`
${N}free memory:  ${G}`free -m | head -n 2 | tail -n 1 | awk {'print $4'}`M / ${G}`free -m | head -n 2 | tail -n 1 | awk {'print $2'}`M
${N}free swap:    ${G}`free -m | tail -n 1 | awk {'print $4'}`M / ${G}`free -m | tail -n 1 | awk {'print $2'}`M
${N}free disk:    ${G}`df -h / | awk '{ a = $4 } END { print a }'` / ${G}`df -h / | awk '{ a = $2 } END { print a }'`

`command -v dokku >/dev/null 2>&1 && dokku ls`${N}


Now when I log back on to my server I’m greeted with an informative status report:

    ____  ___________________   ________
   / __ \/ ____/ ____/ ____/ | / /_  __/
  / / / / __/ / /   / __/ /  |/ / / /
 / /_/ / /___/ /___/ /___/ /|  / / /
/_____/_____/\____/_____/_/ |_/ /_/

uptime:       0 days, 2 hours, 44 minutes

cpu:          Intel(R) Xeon(R) CPU E5-2650L v3 @ 1.80GHz
os:           Ubuntu 16.04.3 LTS GNU/Linux 4.4.0-98-generic x86_64

cpu load:     0.24, 0.10, 0.08
free memory:  55M / 488M
free swap:    1022M / 1023M
free disk:    16G / 20G

-----> App Name           Container Type            Container Id              Status
00-default                web                       2f191ae7dbf4              running
decent.digital            web                       9665b57293a5              running

0 packages can be updated.
0 updates are security updates.

Setting up CI & CD

Automate all the things! If you use Travis CI for testing, you can use it to automatically deploy the app after the build is successful on the master branch (That’s Continuous Delivery™).

See https://jorin.me/deploy-with-travis-and-git/ for a full walkthrough.

Adding more security

See this guide for an extended walkthrough: https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-16-04

Note: This blog post is an update of an old gist I wrote in 2013.