Shared Physics
by Roman Kudryashov
Shared Physics

48 Hours of Migrating Ghost from One URL on AWS to Another URL on Digital Ocean

Published on 5 min read

TLDR: The other day, I had to migrate my ghost installation from one url to another, and from one host to another. Oh boy, was it a pain! So in the hopes that my pain can be someone else's gain, I've put together a checklist of steps and key notes for you to keep in mind when you're doing a migration.

I recently migrated my Ghost installation (the content management system that powers this site) from on AWS to on Digital Ocean.

I did this because my original Roman Design Co domain was created when I was very much focused on design work and I felt that I had outgrown it. Meanwhile, SharedPhysics has been a handle that I've used forever online, and it felt like a good enough catchall for my content (in the spirit of Stratechery, Daring Fireball, Irrational Exuberence, and other personal blogs).

I also migrated because my AWS installation was starting to run up monthly bills that I couldn't understand (it later turned out that I was being billed for unused features, like IP reservations that were free if I was actively using them... oops!), and in the process of troubleshooting, I managed to currupt my installation and lock myself out of shell access (double oops).

But if you're reading this, you're not interested in why I did it. You're interested in how. So here's the how:

On Temporarily Running Two Concurrent Sites

To get started, install Ghost on another site. This one is a bit of a duh, but both AWS and Digital Ocean have 1-click setup paths for Ghost and I recommend following that.

However, I found myself in a bit of a pickle when I installed Ghost the first time around – my domain wasn't yet configured, so I had to install it to the designed IP address instead of a properly configured domain name. After I installed Ghost, I had to go back to GoDaddy and point all of the nameservers over to the new Digital Ocean url, which took roughly 12 hours to propogate.

Once they were pointed correctly, I went back to Digital Ocean and had to fix Ghost to point from an IP address to a proper URL. While you can normally do this via setting the ghost config url or editing the config.json file (located in /var/www/ghost/ for anyone looking), I had a lot of trouble with this because it changed the URL but it didn't set up the SSL certificates. This meant that while the site was being routed correctly, I couldn't access it if my browser had strict certificate validation settings (error message if SSL doesn't check out). Making matters more difficult was that Let's Encrypt/certbot couldn't configure and set up the certificate on its own after I manually changed the config. The solution that worked for me was to rerun the ghost setup CLI from the beginning and to reconfigure everything in one go. This triggered the certbot to correctly configure the certificates and... the site was up!

Transfering Data

Ghost 3.x and up has a feature for exporting all of your data, but it's not done in one go.

Transfering Content & Configurations

If you run Ghost's native export, you're going to get your posts, pages, and primary website settings.

One thing to look out for is that you're not going to get images transfered. This is because images are stored as files and not part of the export json. I had to manually go back through every post and copy/paste my images over.

But be careful here! If you copy the images over from Ghost, you might accidently copy the whole path over – essentially, the problem I created was that my content was pointing at images. Bugger. I had to go through and copy the image – not the image unit in the editor – over, which uploaded the image to my Digital Ocean storage and fixed the url problem.

One other thing to be mindful of is that if you're also switching themes, then you run the risk that some settings configured for one theme might break when you use another theme. I learned this the hard way when I migrated my content, then moved to a new theme that didn't use cover images. However, my old content had cover images, and they started showing up in posts as placeholder blocks without the ability for me to remove them in the editor. I solved this manually by deleting those sections in my content backup, but you can switch  back to your old theme and delete them there too.


In addition to the regular Ghost backups, I like to have html and markdown copies of my content that makes it a bit more human-readable and exportable to other sources. So I'm going to plug my own little GitGhost script so that you can export your content as html and/or back up your content to Git-based workflows. It will take you half a moment to set it up the first time, but then you'll be able to export and back up your content automatically with a scheduled chron job, which is kind of nice!

Transfering Members and Routes

Members and YAML routes (if you ever configured them as a hack in Ghost 3.x editions) need to be transfered over seperately. Ghost offers a native feature for this, but be mindful that the data structure has changed from 3.x to 4.x, meaning that some metadata won't carry over properly.

Redirecting all of your URLs

After migrating all my content and fixing the images problems, I needed to set redirects for all of my posts. You can do this by uploading a redirects json file to ghost in the /ghost/#/settings/labs section.

Since I had a bunch of content, I didn't want to set each url manually (and I tend to not change my urls for canonicalness reasons). I found a neat solution to this problem on stack exchange, recreated here for simplicity. Just write:

    "from": "/([a-zA-Z\\d.-]+)/",
    "to": "$1",
    "permanent": true
    "from": "/",
    "to": "",
    "permanent": true

Just replace with whatever is appropriate for you. The first section will pattern-match for any url to redirect to an identical url with a different primary domain. The second section will redirect your homepage, which was missing from the original stack exchange answer.

Third Party Services

At this point, your site should be up and working, but you're not done yet. You now need to remember all of your third-party services that you're connected to. This includes:

  • Mailgun
    Mailgun is what Ghost uses to send emails, and you'll need to reconfigure new txt, mx, and cname records. It can take up s bit of time for for these to propagate. In addition to setting up the changes in your admin console, you also need to remember to update your config.production.json file with the correct mailgun information. You can read more about how Ghost handles email here.
  • Google Properties
    One of the things that I was really worried about was losing all of my analytics history and SEO juice that's built up over time. Luckily, you can change your URL in Google Analytics properties and keep your history. Similarly, Google Search Console has a "change site address" wizard you can use to migrate one URL to another and keep your ranking in search results.
  • Other Places
    My domain was featured in my LinkedIn, my email, my github, and other places. While the 301 serve as a nice catchall if I forget where I've pasted my content (or for places that linked to me), it is nice to be able to point people to the correct site without redirects. Don't forget to update your social properties!

... and that just about covers it! I hope this helps as a guide for if you're moving from one domain to another. Moving out of AWS/Bitnami has reduced my Ghost hosting costs by half, and setting up/managing Ghost in Digital Ocean has been pleasantly easy in terms of seeing my site performance, costs, and configuration all in one place. If you're thinking of trying it out, I recommend it. You can get $100 of free projects/hosting from them with this link too, so feel free to try it out.

Thanks for reading

Was this useful? Interesting? Have something to add? Let me know.

Seriously, I love getting email and hearing from readers. Shoot me a note at and I promise I'll respond.

You can also sign up for email or RSS updates when there's a new post.