Throughout my blogging experience, I've had access to Azure credits and so I've leaned heavily to Azure solutions. I've recently found myself with no such benefit, which has really levelled the playing field. While I've heard of Digital Ocean (and even had an account), I haven't really taken the leap, until now!

I had the rude experience of having my blog unavailable over a weekend, while away on holiday. I was asked how I had such beautiful error messages (thank you Cloudflare!), but I was left looking for cheap hosting with short notice. I tried Azure first, but the smallest VM was more expensive than others and had a really slow disk. AWS Lightsail was another avenue to consider. GCP also provide good, low-cost compute. I found a few recommendations for Digital Ocean, and that's where I ended up!

One thing I love about Digital Ocean is you can get a 1 core, 1 Gb RAM VM for $5/month (legit!) and they also have managed Kubernetes solutions from $10/node.

1. Create a VM

Creating a VM is super simple!

  1. Select Droplets and click Create.
  2. Select Ubuntu, Basic and $5/month.
  3. Select a Datacenter location. (You might want this near your users if not using CDN)
  4. Generate an SSH Key. (their documentation is great for this!)
  5. Choose a Hostname.
  6. Click Create Droplet.

2. SSH to the VM

You will need to ssh to the machine. I had a little trouble with this because I kept forgetting to use 'root' when connecting. Don't do that.

$ ssh [email protected]<ip>

3. Install Nginx and Docker

We are going to run Ghost in a container and we want Nginx to handle HTTPS for us. Let's go ahead and install Docker and Nginx. (This is the step that really highlights a slow disk!)

$ sudo apt install nginx
$ curl -sL | sh

4. Setup firewall

No one ever got fired for too much security. You COULD enable a cloud based firewall. (most providers have this easily accessible) It never hurts to enable it on the VM too though, right? I'm using UFW (Uncomplicated Firewall). I open up Nginx so that we can use the Nginx config to choose the ports we use. It leaves fewer moving parts.

$ ufw default deny incoming
$ ufw default allow outgoing
$ ufw allow ssh
$ ufw allow 'Nginx Full'
$ ufw enable

$ ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere
Nginx Full                 ALLOW       Anywhere
22/tcp (v6)                ALLOW       Anywhere (v6)
Nginx Full (v6)            ALLOW       Anywhere (v6)

5. Run Ghost

Next, let's run Ghost! It runs on port 2368, which we want to port map to the localhost (-p 2368:2368). Next, we map the localhost volume (/var/www/ to the ghost content folder in the container. Without this, the content would be lost whenever the container restarts. The --restart-always parameter will restart that container, even after a reboot.

$ docker run -p 2368:2368 \
    -e url= \
    -v /var/www/ \
    --restart=always -d ghost:3.33.0-alpine 

You can now use curl to test that your Ghost server is running. Technically, we're hosting Ghost, but it's not on port 80/443 and not accessible to the outside world. Let's fix that.

6. Configure Nginx

We've installed Nginx, but now we need to configure it to redirect port 80 traffic to our Ghost container. We do that by creating a config file in the sites-available folder for each host.

Nginx is a great choice for a reverse proxy. Microsoft have recently released YARP (YARP: Another Reverse Proxy) based on dotnet core. In a future post, I hope to run some performance tests to compare YARP and Nginx.

Create file /etc/nginx/sites-available/ with these contents:

server {
	listen 80;
	location /.well-known/ {
		root /var/www/;

	location / {
	    proxy_set_header    X-Real-IP $remote_addr;
	    proxy_set_header    Host      $http_host;
		proxy_set_header X-Forwarded-Proto https;
	    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

Then enable this by making a soft link to the sites-enabled folder with:

$ ln -s /etc/nginx/sites-available/ /etc/nginx/sites-enabled/
$ sudo systemctl reload nginx

Test with curl localhost -H "Host:".

7. Install and Configure Certbot

To manage HTTPS, we want to use LetsEncrypt to get our certificates for free. There's no such thing as a free lunch, though. LetsEncrypt certificates only last 3 months to encourage people to set up scripts to automate the process.

LetsEncrypt with Nginx can be problematic. It's usually a three-step process, first to set up port 80 with the .well-known path then fetching certificates then configuring SSL with the certificates. After that, you need to work out how to regenerate the certificates every 3 months.

Certbot changes all of that. Once HTTP is working, Certbot will fetch the certificates, update the Nginx config and set up a cron job to renew the certificates as required. Brilliant!

$ apt install certbot python3-certbot-nginx
$ certbot --nginx -d -d

Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, or if you're confident your site works on HTTPS. You can undo this
change by editing your web server's configuration.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel):

Select Option 2, to update the config to force HTTPS. This will update the Nginx config to force HTTP to HTTPS and Certbot will also set up the certificates. Certbot will also set up a timer to update the certificate automatically for you. Check the service with sudo systemctl status certbot.timer to see when it will check next. To test the renewal process, run sudo certbot renew --dry-run.

Don't forget to refresh Nginx to activate the new config.

$ systemctl reload nginx

8. Profit!

That's it! Setup your DNS to point to the IP of your VM and you're good to go. GO ahead and hit the endpoint to confirm. Create a user if this is your first Ghost server.

Backing up

Ghost provides the ability to export your posts, but that won't include any images or themes you've installed.

The best way I know to backup your content is to use SCP. To backup the content folder to a local backup folder, you would do this:

$ scp root:<ip>:/var/www/ backup


Big thanks to the following for their help migrating to Digital Ocean: