How to Migrate a Discourse Forum from DigitalOcean to Linveo
When we launched NeuroBB in 2015, as the first (and only) independent EEG, BCI, and neurofeedback discussion forum, we chose the forum software Discourse. We chose Discourse not because it is easy, but because it is hard because it’s good, despite its hefty resource requirements.
So, I have been self-hosting Discourse on DigitalOcean for the past decade, first on a $5/month 1GB droplet, then on a $12/month 2GB droplet when I ran out of disk space, then back to a $6/month 1GB droplet.
We’ve been happy with DigitalOcean, but it’s a bit pricey for a non-commercial forum. I considered migrating NeuroBB to Flarum to save on hosting, but I wasn’t willing to give up Discourse’s spam protection. (We tested out NodeBB and phpBB before settling on Discourse.) I have been shopping around for a better deal on Discourse hosting for several years now. I hoped to migrate NeuroBB to Fly.io, because I already host a lot of stuff on Fly, but it turned out to be complex, and more expensive than DigitalOcean.
I recently ran across a Bradley Taunt blogpost on Hacker News mentioning a hosting deal from Linveo, a company I hadn’t heard of, offering AMD Ryzen 7950X KVM VPSs with 25gb NVMe disk for $15/year on lowendtalk.com.
It seemed almost too good to be true. Eventually, I fell for it, and signed up for the roomier AMD KVM 2GB VPS (using the lifetime 75% off code LET75AKVM2024 for $25/year). It’s twice the specs of our DigitalOcean droplet, and about a third the price.
I installed Discourse, and it went smoothly. So, I migrated NeuroBB to the new server. It took most of an afternoon. This is exactly how I did it, and how you can do it without repeating my dumb mistakes!
Disclosure: a week after I migrated NeuroBB to Linveo, Linveo became a NeuroBB sponsor, offering us complimentary hosting for the community forum. I drafted this blogpost as I was migrating the site, before I had any affiliation with Linveo. I can’t say anything about Linveo’s support yet, because so far I haven’t needed it.
Prepare your DNS in advance
Before doing anything else, set the TTL (time to live) for your DNS to the minimum possible value (usually 5 minutes). And make sure to update the TTL for your nameservers if you’re changing them. Nameservers can have very high default TTL values.
Do this well in advance of when you plan to switch over the DNS. You have to have changed the TTL longer in advance than the longest old TTL. Because, as far as I can tell, the new TTL is rolled out according to the schedule set by the old TTL!
As you may have guessed, I learned this the hard way. And it’s not the first time I’ve learned this the hard way, either.
Sign up for an account
Discourse will run on either the AMD KVM 1GB VPS for $15/USD year, or AMD KVM 2GB for $25/USD year.
The AMD KVM 1GB plan is approximately equivalent to the $6/month DigitalOcean droplet I was on before. Running Discourse on it is possible, but a bit tight. Expect to run out of disk space when upgrading, and have to manually SSH in and delete old logs and images.
The AMD KVM 2GB is approximately equivalent to the $12/month DigitalOcean droplet, and is more than adequate for a small Discourse community. I thought the extra $10/year was well worth the time and frustration it would save me on upgrades. Also, I wanted to have room to grow, and I doubted the lifetime 75% off deal would last indefinitely.
Whichever plan you choose, use the promo code LET75AKVM2024
for the annual discount.
At the time I signed up, the Ohio and Arizona regions were sold out, so I chose the Texas datacentre.
Create an SSH key pair
You’re going to need to be able to SSH into your server. Linveo will warn you if you haven’t uploaded an SSH key.
I created an SSH key in my .ssh
directory with:
ssh-keygen -C "" -o -a 100 -t ed25519 # create a secure SSH key pair that doesn’t include username and hostname
I recommend naming your public and private keys descriptively. I have so many old SSH keys lying around, it gets confusing. Instead of the default id_CYPHER
, I like to use id_CYPHER_PROVIDER_SITE_PRODUCT
.
Paste contents of your public key (duh) in the provided input on the Linveo dashboard.
Install Ubuntu on Linveo
This is point-and-click in the process of provisioning your new VPS. I used the latest Ubuntu LTS image (Ubuntu 24.04 LTS “Noble Numbat”)
I set the “server name” to neurobb
, and the “hostname” to neurobb.com
. It will prompt you about this.
Install Discourse on Linveo
Okay, it’s time to install Discourse on the new server.
You can find the IP address of your new VPS on the “networking” tab of your dashboard.
SSH in with:
ssh root@$NEW_HOST_IP
Now, just follow the official Discourse cloud install instructions found here.
As far as I can remember, that was:
sudo apt install docker.io
sudo apt install git
sudo apt update && sudo apt upgrade -y && sudo reboot
sudo -s
git clone https://github.com/discourse/discourse_docker.git /var/discourse
cd /var/discourse
chmod 700 containers
Update Discourse on the old server
My DigitalOcean droplet was still on Ubuntu 22.04.3 LTS.
It wasn’t necessary to upgrade the operating system, but it is recommended to update to the latest version of Discourse.
Just for good measure, I ran:
sudo apt update && sudo apt upgrade -y && sudo reboot
... on the old DigitalOcean VPS, and then upgraded Discourse with:
cd /var/discourse && ./launcher rebuild app
Migrate your app.yml
I didn’t want to have to go through the full ./discourse-setup
process.
There were two copies of app.yml on the old VPS, /var/discourse/app.yml
and /var/discourse/containers/app.yml
. This may have been leftover from when I migrated DigitalOcean VPSs and Ubuntu versions. I did some research and decided that containers seemed to be where app.yml belonged, and I diffed the two files and they were identical.
Then, I copied the file from the old host to the new host with:
scp root@$OLD_HOST_IP:/var/discourse/containers/app.yml root@$NEW_HOST_IP:/var/discourse/containers/app.yml
(Notice that I’m using bash variables. That way, you can run export NEW_HOST_IP=PUTYOURIPHERE
, and then the commands can be copy-and-pasted without editing.)
Set the site to read-only
Navigate to https://yoursite.com/admin/backups and click the button in upper right to enable “read only” mode for the forum.
Create a backup of the forum
You can do this from your site’s admin dashboard, or via the command line. For some reason I had trouble getting the backup off the old server when I backed up from the command line (I forget why), so I made a backup from https://yoursite.com/admin/backups, then clicked the download button when it finished, and clicked the link in the email.
Prepare the new host to receive the site export
When you’re SSH’d into the new host, run
mkdir -p /var/discourse/shared/standalone/backups/default
Tip: for this process, I had a three-way TMUX split, with an SSH session to the old DigitalOcean VPS on the left side, my local machine in the middle, and an SSH session to the new Linveo VPS on the right.
Copy the backup to the new host
From the directory with the backup, run:
scp sitename-2019-02-03-042252-v20190130013015.tar.gz root@$NEW_HOST_IP:/var/discourse/shared/standalone/backups/default/
... replacing sitename-2019-02-03-042252-v20190130013015.tar.gz
with the name of your backup.
Restore the backup on the new host
On the Linveo VPS, run:
cd /var/discourse
./discourse-setup
./launcher rebuild app
./launcher enter app
discourse enable_restore
discourse restore sitename-2019-02-03-042252-v20190130013015.tar.gz #do not use path, even if path is correct! just filename!
exit
Notice that comment on the restore command? That’s important. I used find
to locate my backup on the new host, and I passed the correct path to the restore command. It was there, but the restore command couldn’t find it! (And it wasn’t because I was in the Docker container. I found the backup both in and outside the docker container.) The moral of the story is to read the instructions and follow them. Discourse has its way of restoring backups, and it involves putting them a specific folder, and then just passing the filename to the restore command.
Harden the new server
This is a good time to follow the advice Discourse gives on hardening your server.
Install fail2ban, set-up unattended upgrades, and do any other security hardening that you want.
Transfer your DNS
Technically, your new site is running. I probably ran ./launcher rebuild app
a few more times just for fun.
Here’s the problem, though: Discourse is not accessible over an IP address! So there was no way of knowing that the new site was working.
My DNS was with DigitalOcean. I transferred all the records over to my domain registrar (by hand: though DigitalOcean allows exporting zone files, there was no way to import them).
I checked the Discourse logs. Everything looked good. So I decided to be wild and transfer over the nameservers, even though it might lead to a few minutes of downtime.
Okay. Here is where I realized what an idiot I’d been for not updating the TTL before I started! (I had set the TTL to 5 minutes for everything when I transferred the DNS, but the TTL for the nameservers was still very high, which I hadn’t realized!)
After transferring the nameservers to point to my registrar (which was pointing to the new VPS’s IP), and waiting a few minutes, I checked the live site, and ran:
dig neurobb.com
And it still spat out the old DigitalOcean IP. The DNS wasn’t propagating as fast as I expected.
I flushed the DNS caches on my machine, but was still suspicious that maybe I was getting a cached DNS from somewhere.
So, I tried browserling.com.
It showed the new site! It was live!
It also had a message saying that email was disabled other than for staff members.
Fix email configuration
I suddenly remembered this from the last time I migrated Discourse to a new server. Here’s how to fix it:
But there was still an error with email.
On the new server, I ran:
cd /var/discourse && ./discourse-doctor
The doctor was incredibly helpful, and told me exactly what to do.
Following its advice, I edited my app.yml, and changed the Mailgun port from 587
to 2525
. It’s around line 53, the line that says DISCOURSE_SMTP_PORT: 587
.
You should be able to use this line to fix it (make a backup first):
sed -i 's/DISCOURSE_SMTP_PORT: 587/DISCOURSE_SMTP_PORT: 2525/' /var/discourse/containers/app.yml
Then, rebuild:
cd /var/discourse && ./launcher rebuild app
Email worked flawlessly!
Resources
If you’re as dumb as I was, you might find it interesting to watch the DNS propagation in semi-realtime:
⇒ https://dnschecker.org/#A/example.com
It took about a day for my DNS to fully propagate, as far as I recall.
The Discourse forum is the place to find help. For further reading, here are two threads I referred to for the migration process:
⇒ https://meta.discourse.org/t/restore-a-backup-from-the-command-line/108034
⇒ https://meta.discourse.org/t/self-hosting-migration-example-vps-to-vps/146034
You’re done!
Take a backup of the new server from Linveo’s control panel. (I also took one after installing Discourse but before transferring the site, in case I botched the transfer and had to restart.)
Once you’re comfortable with the stability of the new server, shut down the old one.
I hope this helps someone migrate their forum in one hour instead of four, without going grey waiting for their DNS to propagate.