Ghost is an open source publishing platform built on Node.js, used for blogs, newsletters and paid memberships. The officially supported way to run it on your own server is Ghost-CLI, which installs Ghost, configures Nginx, obtains a Let's Encrypt certificate and creates a systemd service for you. In this tutorial you will prepare Ubuntu 24.04 with Node.js, MySQL 8 and Nginx, install Ghost for production, set up transactional email and add a nightly backup.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS with at least 1 GB of RAM (2 GB recommended), for example a CubePath VPS.
- A non-root user with
sudoprivileges. Do not name this userghost: Ghost-CLI creates a system user with that name to run the application. - A domain name with an A record (and AAAA record if you use IPv6) pointing to the server, referred to as
your_domainin this guide. - Ports 80 and 443 open to the internet.
- SMTP credentials from a mail provider, needed in Step 6 for staff invitations and member sign-ups.
Step 1 - Installing Nginx and opening the firewall
Ghost-CLI expects Nginx to be installed before it runs, because it writes the site configuration for you. Update the package index and install it:
sudo apt update
sudo apt install nginx
Allow SSH and web traffic through UFW, then enable the firewall if it is not active yet:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Check the result:
sudo ufw status
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
Nginx Full ALLOW Anywhere
Step 2 - Installing and preparing MySQL 8
Ghost supports MySQL 8 in production (MariaDB is not supported). Ubuntu 24.04 ships MySQL 8.0:
sudo apt install mysql-server
Open the MySQL shell as root. On Ubuntu, the root account authenticates through the system root user, so no password is needed:
sudo mysql
Create a database and a dedicated user for Ghost. Replace your_strong_password with a long random password and keep it at hand for Step 4:
CREATE DATABASE ghost_prod CHARACTER SET utf8mb4;
CREATE USER 'ghost_user'@'localhost' IDENTIFIED BY 'your_strong_password';
GRANT ALL PRIVILEGES ON ghost_prod.* TO 'ghost_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Verify that the new user can log in and see its database:
mysql -u ghost_user -p -e "SHOW DATABASES;"
+--------------------+
| Database |
+--------------------+
| ghost_prod |
| information_schema |
| performance_schema |
+--------------------+
Step 3 - Installing Node.js 22 and Ghost-CLI
Ghost only runs on specific Node.js LTS releases, and the current Ghost release line supports Node.js 22. The version in the Ubuntu archive is older, so install it from the NodeSource repository with a signed keyring:
sudo apt install ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt update
sudo apt install nodejs
Check the installed version:
node --version
v22.20.0
The exact patch version will differ, but it must start with v22. Now install Ghost-CLI globally:
sudo npm install -g ghost-cli@latest
Verify it:
ghost --version
Ghost-CLI version: 1.28.3
Step 4 - Installing Ghost
Ghost-CLI must be run from an empty directory owned by your user. Create one under /var/www, using a name of your choice:
sudo mkdir -p /var/www/ghost
sudo chown "$USER":"$USER" /var/www/ghost
sudo chmod 775 /var/www/ghost
cd /var/www/ghost
Start the installer. It checks the system first, then downloads the latest Ghost release and asks a series of questions:
ghost install
Answer the prompts as follows:
| Prompt | Answer |
|---|---|
| Enter your blog URL | https://your_domain |
| Enter your MySQL hostname | localhost |
| Enter your MySQL username | ghost_user |
| Enter your MySQL password | the password from Step 2 |
| Enter your Ghost database name | ghost_prod |
| Do you wish to set up Nginx? | Y |
| Do you wish to set up SSL? | Y |
| Enter your email (For SSL Certificate) | your email address |
| Do you wish to set up Systemd? | Y |
| Do you want to start Ghost? | Y |
Because you entered https:// in the URL, Ghost-CLI requests a Let's Encrypt certificate for your_domain, writes an HTTP to HTTPS redirect, and schedules certificate renewal automatically. You do not need to install Certbot. The DNS record must already resolve to this server, or the SSL step fails.
When the installer finishes, run ghost ls to list the installation. The status column must read running (production) and the process manager systemd.
Ghost listens only on 127.0.0.1:2368; Nginx proxies public traffic to it. Confirm the site answers over HTTPS:
curl -sI https://your_domain | head -n 1
HTTP/2 200
Step 5 - Creating the owner account
Open https://your_domain/ghost in a browser. Ghost shows the setup screen where you enter the site title, your name, email address and a password. The first account created here becomes the site Owner, with full control over settings, staff and billing, so do this immediately after the installation.
Step 6 - Configuring email delivery
Ghost sends two kinds of email: transactional mail (staff invitations, password resets, member sign-in links) configured on the server, and bulk newsletters, which are configured separately in the admin panel through a Mailgun integration. Without transactional mail, you cannot invite staff and members cannot sign in.
Open the production configuration file:
nano /var/www/ghost/config.production.json
Add a mail block at the top level of the JSON object, next to url, server and database. Replace the host, port, user and password with the values from your mail provider:
"mail": {
"transport": "SMTP",
"from": "'Your Site' <noreply@your_domain>",
"options": {
"host": "smtp.your_provider.com",
"port": 587,
"secure": false,
"auth": {
"user": "your_smtp_user",
"pass": "your_smtp_password"
}
}
},
Use "port": 465 with "secure": true if your provider only offers implicit TLS. Check that the file is still valid JSON, then restart Ghost:
python3 -m json.tool /var/www/ghost/config.production.json > /dev/null && echo OK
cd /var/www/ghost && ghost restart
To test delivery, go to Settings > Staff in the admin panel and invite a user with an address you control. If the email does not arrive, ghost log in /var/www/ghost shows the SMTP error.
Step 7 - Scheduling daily backups
A Ghost backup consists of two parts: the MySQL database and the content directory, which holds images, themes and uploaded files. Store the database credentials in an option file readable only by root, so the password does not appear in the script or in the process list:
sudo nano /root/.ghost-backup.cnf
[mysqldump]
user=ghost_user
password=your_strong_password
sudo chmod 600 /root/.ghost-backup.cnf
Create the backup script:
sudo nano /usr/local/bin/ghost-backup
#!/usr/bin/env bash
set -euo pipefail
backup_dir="/var/backups/ghost"
ghost_dir="/var/www/ghost"
stamp="$(date +%Y%m%d-%H%M%S)"
mkdir -p "$backup_dir"
chmod 700 "$backup_dir"
mysqldump --defaults-extra-file=/root/.ghost-backup.cnf \
--single-transaction --no-tablespaces ghost_prod \
| gzip > "$backup_dir/ghost-db-$stamp.sql.gz"
tar -czf "$backup_dir/ghost-content-$stamp.tar.gz" -C "$ghost_dir" content
# Keep 14 days of backups
find "$backup_dir" -type f -name 'ghost-*' -mtime +14 -delete
Make it executable and run it once by hand:
sudo chmod 750 /usr/local/bin/ghost-backup
sudo /usr/local/bin/ghost-backup
ls -lh /var/backups/ghost
-rw-r--r-- 1 root root 5.8M Sep 25 02:00 ghost-content-20260925-020000.tar.gz
-rw-r--r-- 1 root root 412K Sep 25 02:00 ghost-db-20260925-020000.sql.gz
Schedule it every night at 02:30 with a file in /etc/cron.d:
echo '30 2 * * * root /usr/local/bin/ghost-backup' | sudo tee /etc/cron.d/ghost-backup
Copy these files to storage outside the server (object storage or another host) so that a lost VPS does not also mean lost backups.
Keeping Ghost up to date
Ghost releases often. Before updating, run a backup, then let Ghost-CLI download the new version, run database migrations and restart the service:
sudo /usr/local/bin/ghost-backup
cd /var/www/ghost
ghost update
If an update causes problems, ghost update --rollback returns to the previous version.
Troubleshooting
ghost install stops at the system checks. Read the message: the usual causes are running it as root, using a user named ghost, a non-empty directory, or an unsupported Node.js version. Fix the cause and run ghost install again in an empty directory.
The SSL step fails. Let's Encrypt must reach http://your_domain on port 80. Check the DNS record with dig +short your_domain, make sure UFW allows Nginx Full, then run ghost setup ssl from /var/www/ghost to retry.
502 Bad Gateway. Nginx is running but Ghost is not. Run ghost status and ghost log in /var/www/ghost, or check the unit directly with sudo journalctl -u ghost_your-domain -n 50 (the service name is ghost_ followed by the domain with dots replaced by dashes).
Conclusion
Ghost is now running in production on Ubuntu 24.04 behind Nginx with HTTPS, a dedicated MySQL user, working transactional email and nightly backups. Next, install a theme from Settings > Design, configure a Mailgun integration if you plan to send newsletters, and copy your backups to off-site storage.
