The Autodidacts

Exploring the universe from the inside out

Host a Ghost 5.0 Site on Fly.io Free Tier In 2 Minutes — MySQL 8 edition

Host a Ghost Blog Free on Fly.io

Not long ago, I wrote a post on how to deploy a Ghost 5 blog on Fly.io without paying a dime. Shortly after, Ghost removed support for SQLite in production, in favour of MySQL.

However, hosting Ghost + MySQL 8 on Fly turned out to be a bit gnarly. MySQL 8 requires ~2GB of RAM, and Fly's free tier has 256MB. MySQL wouldn't even start. And I couldn't find anybody who had gotten it running successfully.

Note: instead of repeating myself, I'll let you read my previous post (if you haven't yet) for background on Ghost, Fly, and why any of this is even relevant. If you just want to play around with Ghost, that tutorial still works for running Ghost in development mode on Fly's free tier.

After much fruitless MySQL tuning, always getting the dreaded OOM (out of memory) error, I asked for help on the Fly forum — and Fly’s Chris Fidao and Will Jordan came up with a solution that got MySQL 8 running on the free tier with ~5MB of memory to spare. (They have also helpfully updated the MySQL tutorial in the fly docs with the instructions for how to stay within the free tier's memory limits, which will be useful for people trying to run Wordpress or other LAMP applications on Fly.)

I have created a new version of the one-shot copy-and-pastable install command from my last post. This updated script spins up an application instance with Ghost 5, and a database instance with MySQL 8 with the necessary performance tweaks. No need to futz around manually editing config files; you'll be up and running in minutes:

# Prompt for app name
echo -n "Enter App Name: " && \
read appname && \
echo -n "Enter MYSQL password: " && \
read mysqlpassword && \
echo -n "Enter MYSQL Root password: " && \
read mysqlrootpassword && \
# Heredoc wrapper for tidyness (starting now, because we can't seem to prompt for input within heredoc)
bash << EOF
# Uncomment following line for debugging (prints each command before running)
set -x
# Check if Fly CLI is installed, and install it if it isn't
# Inspect script first if you are leery of pipe-to-bash
command -v flyctl >/dev/null 2>&1 || { echo >&2 "Fly CLI required and not found. Installing..."; curl -L https://fly.io/install.sh | sh; }
# This will open a browser, where you can enter a username and password, and your credit card (which is required even for free tier, for fraud prevention).
flyctl auth signup
# Create a directory for the project and enter it, since the next command will output a file
mkdir ghost-flyio && cd ghost-flyio
# Create an app -- using Ghost docker image, Seattle region, and app name prompted for earlier -- but don't deploy it
flyctl launch --name $appname --image=ghost:5-alpine --region sea --no-deploy --org personal
# Provision a volume for Ghost's content
# Size can be up to 3GB and still fit in the free plan, but 1GB will be enough for starters.
flyctl volumes create ghost_data --region sea --size 1 --auto-confirm
# Install sed (stream editor), if it isn't already installed
command -v sed >/dev/null 2>&1 || { echo >&2 "sed (Stream EDitor) required and not found. Installing..."; sudo apt install sed; }
# Update the port to Ghost's default (2368)
sed -i 's/internal_port = 8080/internal_port = 2368/g' fly.toml
# Append info about where to find the persistent storage to fly.toml
cat >> fly.toml << BLOCK
[mounts]
  source="ghost_data"
  destination="/var/lib/ghost/content"
BLOCK
# Set Ghost url
flyctl secrets set url=https://$appname.fly.dev

# Spin up a second instance for our database server
mkdir ghost-flyio-mysql && cd ghost-flyio-mysql
# Create an app for our mysql
flyctl launch --name ${appname}-mysql --image=mysql:8 --region sea --no-deploy --org personal  
# If you aren't trying to stay within the free tier, uncomment this to give the MYSQL VM 2GB of ram
# fly scale memory 2048
# Create a persistent volume for our MYSQL data
fly volumes create mysql_data --size 1 --region sea --auto-confirm
fly secrets set MYSQL_PASSWORD=$mysqlpassword MYSQL_ROOT_PASSWORD=$mysqlrootpassword
# Add our database credentials to the Ghost instance
fly secrets set database__client=mysql
fly secrets set database__connection__host=${appname}-mysql.internal -a $appname
fly secrets set database__connection__user=ghost -a $appname
fly secrets set database__connection__password=$mysqlpassword -a $appname
fly secrets set database__connection__database=ghost -a $appname
fly secrets set database__connection__port=3306 -a $appname
fly secrets set NODE_ENV=production
cat > fly.toml << BLOCK2  
app = "$appname-mysql"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []
[build]
  image = "mysql:8.0.32"
[mounts]
  source="mysql_data"
  destination="/data"
[env]
  MYSQL_DATABASE = "ghost"
  MYSQL_USER = "ghost"
[processes]
  app = "--datadir /data/mysql --default-authentication-plugin mysql_native_password --performance-schema=OFF --innodb-buffer-pool-size 64M"
BLOCK2
# Note: if you aren't trying to stay within the free tier, you might want to remove the performance schema and buffer pool size flags above.
# Deploy MySQL server.
flyctl deploy
cd ../
# Deploy Ghost application server
flyctl deploy
# Boom! We're airborne.
# End our bash Heredoc
EOF

This blog is running Ghost 5 + MySQL 8 in production on Fly’s 256MB free tier, using the configuration described in this article.

[Shameless plug] once you have your blog up and running, you might want to grab one of my downright gorgeous [citation needed] Ghost themes: Weblog (premium), MNML (free & open source), Laminim (premium), or Undefined (free & open source). Or, if you just want to support us, you can … use our referral link to sign up for the paid Ghost(Pro) hosting that our tutorials make unnecessary? Seems legit!