I had a need to migrate this server to a different provider for cost reasons. This was also a good chance to document my process since I did NOT do that last time.

I ended up going with Digital Ocean.  Amazon, Linode, and Digital Ocean all had the same specs for the same price.  Honestly the thing that got me to pick Digital Ocean was simply the number of times I had used their documentation in the past to do server maintenance tasks. Their documentation is always up-to-date, easy to follow, and exactly what I am looking for at the time.

To migrate, I had a handful of tasks I needed to complete:

  • Spin up, do basic configuration, and secure the server
  • Install/Configure NGINX (reverse proxy)
  • Install/Configure MySQL (database)
  • Install/Configure Node.JS (Ghost JS engine)
  • Install/Configure/Restore Ghost
  • Install/Configure/Restore Confluence
  • Update DNS for the new server
  • Update my backup scripts

Spinning Up The New Server

Spinning up the server in Digital Ocean was super easy.  The only thing that I did not like was that if you add an SSH key at deployment, it is for the root user.  I would have liked to be able to kick the server with a non-admin user as well.

I also configured Digital Ocean monitoring for:

  • Inbound/Outbound bandwidth over 70Mbps for 5 minutes
  • CPU over 70% utilization for 5 minutes
  • Disk I/O over 70% utilization for 5 minutes


First thing was to get everything updated: apt-get update && apt-get upgrade -y

Then set up automatic updates.  I followed this article. In Ubuntu 18, unattended-upgrades is installed by default.  In the /etc/apt/apt.conf.d/50unattended-upgrades file I also uncommented the updates line. In /etc/apt/apt.conf.d/20auto-upgrades I just added the two missing lines from their example into my file.

Create Low Privilege User

useradd -m -s /bin/bash trevor
# Copy the key I uploaded at creation
cp .ssh/authorized_keys /home/trevor/.ssh/
chown trevor:trevor /home/trevor/.ssh/authorized_keys
usermod -aG sudo trevor
su trevor

Update SSH Config

Port 22 gets scanned and probed like it is going out of style. So I like to avoid that, updating these lines in /etc/ssh/sshd_config

# My super secret SSH port (not really)
Port 12345
PermitRootLogin no
PubkeyAuthentication yes


UFW is installed by default in Ubuntu 18 so I just needed to add some rules. Specifically I want to allow SSH on my port and since I am going to be using Cloudflare, I only want to allow HTTP(S) traffic from their IPs. There is a good script here to populate the Cloudflare IPs.

ufw allow 12345
sudo ufw default deny incoming
sudo ufw default allow outgoing

sudo ./cloudflare-ufw.sh

# Add this line to crontab
0 0     * * 1   root    /home/trevor/cloudflare-ufw/cloudflare-ufw.sh > /dev/null 2>&1


Followed there two guides:

The only considerations were to update the SSH port to my custom port, add my home IP (kind of static) to the ignoreip directive, and change the ban actions to use ufw.conf.

SSMTP and Sendmail

I had to install both in Ubuntu 18 with apt-get install. Then I copied over my config file from the older server in /etc/ssmtp/ssmtp.conf.

Installing Required Software


Because I had already done this on my old server, this part was fairly simple.

sudo apt-get install nginx

Copy config from old server in /etc/nginx/sites-available/ghost.conf

sudo ln -s /etc/nginx/sites-available/ghost.conf /etc/nginx/sites-enabled/ghost.conf

Generate new cert from Cloudflare Origin Cert under the Crypto tab to enable proper (strict) TLS between Cloudflare and my server.  Update NGINX config to use this cert and key by updating any ssl_certificate or ssl_certificate_key lines in /etc/nginx/sites-available/ghost.conf with the appropriate files.

To ensure ONLY Cloudflare can access the server, I also installed the client certificate available here.  Place that certificate in /etc/ssl/certs/cloudflare.crt and add these lines to the appropriate server blocks in /etc/nginx/sites-available/ghost.conf

ssl_client_certificate /etc/ssl/certs/cloudflare.crt;
ssl_verify_client on;

Edit /etc/nginx/nginx.conf and /etc/nginx/sites-available/ghost.conf to remove TLSv1 and TLSv1.1.


Just followed this guide.


Just follow their guide being sure to follow the steps to install the Build Tools.


Follow the Ghost Install Guide.  I did have some issues with files in my home directory not having proper permissions, I think due to installing Node with sudo. Running chown -R trevor:trevor /home/trevor did the trick.

In the guide, I skipped the UFW section since I already did that. Once the install is complete, login to the Ghost web portal to finish initial configuration.  Due to my firewall rules and NGINX config, I had to set up a local SSH tunnel to the internal Ghost instance.

From here I exported the blog content using the native export function in the Labs tab. This export does NOT include images, so that have to be copied separately.

# Copy image to local host
scp -r ghost:/var/www/ghost/content/images ./images
# Copy inmages from local host to new server
scp -r images/ ghost2:~
sudo mv ~/images/* /var/www/ghost/content/images
# Reset permissions
chown ghost:ghost /var/www/ghost/content

Update /var/www/ghost/config.production.json with the URL of your blog so that any internal links do not point to localhost.

Import the exported JSON content from the old blog and check any warnings that are produced.  All of my warnings were benign and all data was properly transferred.

Delete the default Ghost posts.

Under the design tab, ensure that the links for any sections that are configured get updated to the public link instead of the internal link.

Ghost Tweaks

I also did a couple tweeks to the base installation.

  • Added PrismJS code syntax highlighting per this guide.
  • Added Disqus comments per this guide
  • Added Table Of Contents to all my posts per this guide. This one required some additional tweaking. In the toc.js file I had to make these changes:
    • Line 10: Change value to 0. This seems to go along with including H1 and just makes the output format look correct
    • Line 57: Per your comment, added h1 to the list.
    • My full section from post.hbs looks slightly different:
<section class="post-full-content">
<div id="toc"></div>

<div class="post-content">

{{!-- Table of Contents --}}
<script type="text/javascript">
var toc = document.getElementById('toc');
toc.innerHTML = getTocMarkup(document);


In the old Confluence installation, login as a system admin and create a new backup in the General Configuration > Backup and Restore section and download that to your local host. During the install process, you will be asked for this.

Get the binary download link here.

wget https://www.atlassian.com/software/confluence/downloads/binary/atlassian-confluence-6.11.2-x64.bin
chmod +x atlassian-confluence-6.11.2-x64.bin
sudo ./atlassian-confluence-6.11.2-x64.bin

During install, all defaults can safely be accepted. Once the installer is complete,  install the MySQL drivers. And also take care of Step 2 here.

wget https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.47.tar.gz
sudo cp mysql-connector-java-5.1.47/mysql-connector-java-5.1.47-bin.jar /opt/atlassian/confluence/confluence/WEB-INF/lib
sudo /etc/init.d/confluence restart

Now browse to the web interface (again needing an SSH tunnel) and follow the setup instructions. On the database configuration page, select MySQL and use the "Connection String" method. Your connection string will look something like this (input proper DB name): jdbc:mysql://'READ-COMMITTED'

At the step to import from a backup, select the file downloaded earlier.

Login using the old user/pass and go to General Configuration to update the "Base URL".  Also follow this article to update the Confluence proxy settings as well.

In /opt/atlassian/confluence/conf/server.xml I needed to add address="ip6-localhost" to the Connector block to force Confluence to only listen on localhost.

Finally, for my backups I needed to add my standard user to the Confluence group: sudo usermod -aG confluence trevor.


Follow the guide here to install OSSEC-HIDS as a local installation.

Follow this guide to configure POSTFIX to send OSSEC emails.  I also changed inet_interfaces in main.cf to so that I did not have an SMTP server listening to the world.


Now that everything was set up and running, time to cut over my DNS configuration.  This was amazingly easy: log in to Cloudflare, update IPs...done!  The changes were almost immediate.


At this point I needed to move my backup scripts over to the new server, update my NAS rsync jobs to point to the new IP, and copy over my cronjobs. This also required a new SSH key pair for my NAS.