The New Digs: My Server Setup
June 6, 2018

When I learned that Microsoft had acquired GitHub, I first thought about migrating to Bitbucket because they have unlimited free private repos. But then I remembered that I had tried building a public site using their “pages” feature (similar to GH Pages) several years ago, but had been disgusted by the fact that the pages always came wrapped in an iframe. So, I tried migrating to GitLab a couple of days ago. At first, all seemed well, but the process of getting their “pages” feature working was an absolute pain in the ass. Whereas GH Pages could be up and running in two clicks, GL Pages required config files and pipelines, none of which I understood. I’m sure they want to provide a greater degree of customization than GH, but they should still provide a few one-size-fits-all sane defaults for popular static site generators. Plus, their documentation didn’t match my experience; the tutorials and examples were scattered around, and they didn’t all match. I ended up turning to StackOverflow for help. And when I finally got it all going, Jekyll’s blog links were broken and could only be fixed by appending “.html” to each post URL. So, in the end, I decided to push my repos to Bitbucket and to build an Express server on a DigitalOcean droplet to host my sites. This site, for example, is running on that server. I’m writing this post primarily to document how I did it so that I can remember later when I inevitably have to relearn the process. So, here it is!

1. Basic Setup

I created the cheapest possible droplet on DO. It’s running Ubuntu 16.04 and costs $5 per month plus a little for outgoing bandwidth. Then I went to my domain registrar and pointed my domains’ A records to the server’s IP address.

On the server, I started by creating a sudo user.

adduser josh
usermod -aG sudo josh

Then I logged in with that user and installed LinuxBrew.

sudo apt-get install build-essential curl file git
sh -c "$(curl -fsSL https://raw.githubusercontent.com/Linuxbrew/install/master/install.sh)"

Then I used LinuxBrew to install Node and Ruby, and then Jekyll and forever.

brew install node ruby
gem install jekyll
npm install -g forever

I knew I’d need to serve my sites over https, so I installed Certbot, which supplies Let’s Encrypt certificates.

sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbot

Also, since I wanted to be able to connect to my Bitbucket repos over SSH, I copied the SSH keys from my machine to the server.

# (from local machine)
scp -r ~/.ssh/* root@<SERVER_IP_ADDRESS>:/home/josh/.ssh/

2. Let’s Encrypt

To prepare for getting my TLS certificates, I made a folder that would eventually contain all of the interesting server stuff, which I just called mother-base. I moved into that folder, made a folder called public, and wrote a little Express app. This app, as you can probably tell, simply serves the public folder on port 8080. I called this file tls.js.

var express = require("express");
var path = require("path");

var app = express();
app.use(express.static(path.join(__dirname, "/public")));
app.listen(8080);

However, the Certbot process works over port 80, not 8080. So, I had to reroute port 80 to 8080 using iptables.

sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080

I ran this app with forever.

forever start tls.js

Then, I ran Certbot to get my TLS certificates. Since I planned to use multiple domains on this server, it was necessary for me to list all of those domains in this single command so that all of the domains would end up on the same certificate. I also learned the hard way that Certbot only allows 5 failed certification attempts per hour. I made lots of little mistakes along the way and got locked out for an hour. But then I learned that it’s possible to use the --dry-run flag to test out the process before spending a real attempt.

sudo certbot certonly --webroot -w ./public -d ameyama.com -d www.ameyama.com

Then I stopped the forever job.

forever stopall

The new certificates were inaccessible to my user, even with sudo privileges. For example, sudo /etc/letsencrypt/live failed. So, I had to fix the permissions.

sudo chmod -R 755 /etc/letsencrypt/live
sudo chmod -R 755 /etc/letsencrypt/archive

Then, I created a folder called tlscert in which I created symlinks to the relevant certificate files.

sudo ln -s /etc/letsencrypt/live/ameyama.com/fullchain.pem ./tlscert/
sudo ln -s /etc/letsencrypt/live/ameyama.com/privkey.pem ./tlscert/

3. Submodules

After turning the mother-base folder into a git repo, I added my websites as submodules, cloned them into the public folder, and committed them to the mother-base repo.

git submodule add git@bitbucket.org:jrc03c/ameyama.com.git ./public/ameyama.com
git submodule init
git submodule update
git add . --all
git commit -m "Added ameyama.com as a submodule."

Because this site is built with Jekyll, I moved into the newly-cloned folder and built it.

cd public/ameyama.com
jekyll build
cd ../..

4. Express

Here’s the whole Express server.

var path = require("path");
var express = require("express");
var http = require("http");
var https = require("https");
var fs = require("fs");
var helmet = require("helmet");
var vhost = require("vhost");
var serveStatic = require("serve-static");
var app = express();

// use helmet for basic security stuff
app.use(helmet());

// this config object tells serveStatic to try
// adding the .html extension if the requested
// path doesn't exist and to ignore requests
// for dotfiles
var config = {
  extensions: ["html"],
  dotfiles: "ignore",
};

// use vhost (in combo with serveStatic) to host
// multiple domains in the same express app
app.use(vhost("ameyama.com", express().use("/", serveStatic("public/ameyama.com/_site", config))));

// redirect http to https
var http = express();

http.get("*", function(request, response){
  response.redirect("https://" + request.hostname + request.url);
});

http.listen(8080, function(){
  console.log("Listening on port 8080 to redirect http traffic...");
});

// run!
https.createServer({
  key: fs.readFileSync(path.join(__dirname, "/tlscert/privkey.pem")),
  cert: fs.readFileSync(path.join(__dirname, "/tlscert/fullchain.pem")),
}, app).listen(8443, function(){
  console.log("Listening on port 8443...");
});

Once that was set up, the only thing left to do was to configure iptables to forward port 443 to port 8443.

sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8443

5. Going Forward

There are still a few things I’d like to fix.

So, yep. That’s it!