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
Arecord already points to the VPS beforeghost 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 userghost, 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.