Skip to content
+1 (813) 212-3723 support@layeronecloud.com
Self-hosting VPS guides

How to host a production Ghost blog on an Ubuntu VPS

Install Ghost with the official Ghost-CLI stack—Ubuntu, MySQL, Node.js, Nginx, systemd, and Let's Encrypt—with realistic VPS sizing and backups.

6 min read

Ghost is a publishing and membership platform built on Node.js. Its official production path uses Ghost-CLI to configure MySQL, Nginx, Let's Encrypt, and systemd on Ubuntu. This guide follows that path instead of substituting an unofficial all-in-one container.

Ghost-CLI installations support the core publication and membership features. Ghost's newer self-hosted analytics and fully self-hosted ActivityPub services use a separate Compose-based installation path that upstream currently describes separately; budget more resources if you choose it.

VPS size, CPU, storage, and price

Ghost officially requires a server with at least 1 GB of memory for this stack.

Item Planning value
Minimum RAM 1 GB upstream minimum
Recommended RAM 2 GB for MySQL, Node.js, Nginx, updates, and modest traffic headroom
Expected CPU usage Roughly 0.05–0.20 vCPU idle; 0.5–1 vCPU during uncached traffic, image processing, imports, updates, or newsletter work
Storage requirement Allow 10 GB minimum for Ubuntu and the stack; 20 GB or more for themes, images, audio/video, database growth, logs, and backup staging
Exact LayerOne SKU gc.micro — slug layerone-starter, 1 vCPU, 2 GB RAM, 40 GB disk
Expected monthly cost $0.0068 per active hour; about $4.96 per 730 hours against the advertised $5.00 730-hour equivalent

A 744-hour month is about $0.0068 × 744 = $5.06. Audience size, caching, email sends, media, transfer, and storage growth vary, and LayerOne bills the allocated VPS hourly. CPU ranges are planning estimates, not a traffic guarantee. Large newsletters and analytics services should be sized and tested independently.

Assumptions

  • A fresh Ubuntu 24.04 LTS LayerOne VPS
  • A public IPv4 address and a registered domain such as blog.example.com
  • The DNS A record already points to the VPS before ghost install
  • SSH key access and a fixed administration source address called ADMIN_IP
  • A new non-root username called publisher; upstream warns not to name this operating-system user ghost, because that conflicts with Ghost-CLI

1. Harden the network boundary

In the LayerOne cloud firewall, set inbound default DROP and allow TCP 22 from ADMIN_IP/32, plus TCP 80 and 443 from 0.0.0.0/0. MySQL port 3306 and Ghost's internal Node port must never be public.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from ADMIN_IP to any port 22 proto tcp
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status verbose

Keep the equivalent LayerOne cloud firewall rules even though UFW is enabled.

2. Create the non-root operator

Run this as root, choosing a unique password when prompted even though normal administration should use an SSH key:

adduser publisher
usermod -aG sudo publisher
install -d -m 0700 -o publisher -g publisher /home/publisher/.ssh
cp /root/.ssh/authorized_keys /home/publisher/.ssh/authorized_keys
chown publisher:publisher /home/publisher/.ssh/authorized_keys
chmod 600 /home/publisher/.ssh/authorized_keys
su - publisher

Open a second terminal and prove ssh publisher@SERVER_IP and sudo -v work before disabling root/password SSH as described in Harden a new server.

3. Install Nginx and MySQL 8

sudo apt-get update
sudo apt-get full-upgrade -y
sudo apt-get install -y nginx mysql-server
sudo systemctl enable --now nginx mysql
sudo mysql_secure_installation

Ghost-CLI needs a password-authenticated MySQL administrator during initial database creation. Generate and save a strong MySQL root password, then run:

sudo mysql

At the MySQL prompt, replace the placeholder with the saved random password:

ALTER USER 'root'@'localhost'
  IDENTIFIED WITH 'mysql_native_password' BY 'REPLACE_WITH_RANDOM_PASSWORD';
FLUSH PRIVILEGES;
EXIT;

Do not paste the real value into shell history, tickets, or documentation. The Ghost installer can use this account once to create a restricted database user for the publication.

4. Install the supported Node.js and Ghost-CLI

Ghost's current Ubuntu guide uses Node.js 22. Add the signed NodeSource repository exactly as upstream documents:

sudo apt-get update
sudo apt-get install -y 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
NODE_MAJOR=22
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main"   | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt-get update
sudo apt-get install -y nodejs
sudo npm install ghost-cli@latest -g
node --version
ghost --version

Before a later major Ghost upgrade, re-check the supported Node and MySQL versions rather than assuming these remain current forever.

5. Install Ghost

sudo mkdir -p /var/www/blog.example.com
sudo chown publisher:publisher /var/www/blog.example.com
sudo chmod 775 /var/www/blog.example.com
cd /var/www/blog.example.com
ghost install

Answer the installer prompts as follows:

Prompt Value
Blog URL https://blog.example.com
MySQL hostname localhost
MySQL username root for this one-time setup
MySQL password The protected password created above
Database name Accept a descriptive generated/default name
Set up a Ghost MySQL user Yes
Set up Nginx Yes
Set up SSL Yes, with a monitored certificate email address
Set up systemd Yes
Start Ghost Yes

Using the domain rather than an IP is required for the normal production and certificate flow. Ghost-CLI writes the Nginx and systemd configuration and keeps the application process unprivileged.

6. Verify and claim the publication

cd /var/www/blog.example.com
ghost status
ghost doctor
curl -I https://blog.example.com
sudo nginx -t

Open https://blog.example.com/ghost/ and create the owner account. Publish a draft, upload an image, preview the site, and send a test email through the mail provider you configure. Nginx returning 200 does not prove Ghost can write media, connect to MySQL, or send mail.

Use a transactional email provider for production delivery. Running a full mail server on the same VPS adds DNS reputation, reverse DNS, queue security, and abuse-management work that this guide does not cover.

Security pitfalls

  • Never expose MySQL 3306 or Ghost's local Node listener.
  • Keep owner/staff accounts individual; do not share one administrator login.
  • Rotate Admin API keys and integration secrets that appear in logs or build systems.
  • Test theme updates in staging. A theme ZIP executes templates against your publication and can break rendering after a major version update.
  • Protect member exports and database dumps: they contain personal data and may contain Stripe customer references.
  • Install Ubuntu security updates and monitor disk, RAM, HTTPS, and the Ghost process from outside the VPS.

Back up, restore, and update

From the publication directory, Ghost-CLI can create a ZIP containing content, members, themes, media, routes, and redirects:

cd /var/www/blog.example.com
ghost backup

Also take a native MySQL dump for a full operational recovery. Substitute the database name printed by ghost config database.connection.database:

cd /var/www/blog.example.com
ghost config database.connection.database
sudo install -d -m 0700 -o publisher -g publisher /var/backups/ghost
sudo mysqldump -u root -p --single-transaction --routines --triggers   DATABASE_NAME | gzip > /var/backups/ghost/ghost-mysql-$(date +%F).sql.gz

Copy both backups off the VPS, encrypt them, and test restoration into a separate Ghost instance. LayerOne does not take snapshots or backups; see Backups and snapshots.

For updates within a supported path:

cd /var/www/blog.example.com
ghost check-update
ghost backup
sudo npm install ghost-cli@latest -g
ghost update
ghost status
ghost doctor

Major versions can include theme, Node.js, MySQL, and feature changes. Follow the exact upstream major-update path and keep the tested backup until rollback is no longer needed.

Official and LayerOne references

Still stuck

Chat with us from the portal.

Ask the assistant from the Chat bar. During business hours you can ask for a person and a human joins live.