Ghost 6
Overview
Ghost is a free and open source publishing platform written in Node.js. It offers a fast editor, posts and pages, themes, tags, staff accounts with roles, and built-in members and newsletters. Its admin lives at /ghost/ on your site.
This guide covers deploying and using the SecureAnyCloud Ghost 6 on Ubuntu 24.04 LTS image on Microsoft Azure. The image runs Ghost 6.73.0 and is set up so that no one but you can claim the site.
What you get:
- No open setup wizard, no shared database, no shared keys. A stock Ghost asks the first visitor to
/ghost/to create the site owner, so whoever reaches a new site first owns it. This image ships no database at all (the MySQL data directory is empty) and no secret in Ghost's configuration. On your VM's first boot, the image does the following. It initializes its own MySQL. It creates Ghost's database user with a password generated on your VM. It runs Ghost once on a random local port, so that Ghost's migrations generate this site's session secrets, member and staff signing keys and API keys. It then creates the owner account through Ghost's own setup API over the local loopback interface, with a password generated on your VM. Finally, it checks that Ghost reports setup as complete and refuses a second setup. Only after that do Ghost and nginx start. If first boot cannot finish, nothing is served on ports 80 and 443. - Secrets of your VM's own, never on a command line. The owner password, the database password, Ghost's keys and the TLS key are generated on your VM, and nothing from the image build is reused. Passwords travel in files only root can read (or on standard input), never as command-line arguments. The database password reaches Ghost as a systemd credential, so it is not in
config.production.jsonor in Ghost's environment. - The current, verified release. Ghost 6.73.0 is checked file for file against the official npm release, pinned by its checksum. It includes body-parser 1.20.9, the fix for GHSA-27r3-jxmh-p828, and every Ghost security fix up to this release. Node.js 22.23.4 (the October 2026 security release) comes from NodeSource's signed repository.
- HTTPS from the first boot. nginx serves Ghost on port 443 with a certificate generated on your VM's first boot. It is a self-signed certificate for this VM only, so it cannot vouch for any other site. Port 80 redirects to HTTPS. Ghost's own address is pinned to your VM's public IP address (or the name you set in Step 11), so every link, emails included, uses it.
- No calls home and no third-party scripts. Ghost's daily update check, Gravatar, structured data pings, the social web (ActivityPub), Unsplash, web analytics and the Explore ping are off. Portal (sign-up and sign-in), Search, Comments and Ghost's other front-end scripts load from your own VM (pinned releases) instead of from jsDelivr, and the default cover images Ghost points at static.ghost.org are removed. Ghost's admin, in your staff's own browsers, still fetches a few public assets from other servers: Ghost's changelog from ghost.org, default profile pictures from static.ghost.org, font previews from fonts.bunny.net (in Settings > Design & branding) and, when the editor's emoji picker is opened, emoji data from jsDelivr.
- Webmention receipt off. Ghost fetches the source address of every webmention anyone sends it. The image refuses incoming webmentions (Step 9 shows how to turn them back on).
- Members sign-up by invitation until you configure email. Ghost's members sign in with emailed links, and the image has no mail service. Sign-up is set to invite-only. Staff device verification (an emailed code on sign-in from a new browser) is off so the owner is never locked out. Step 8 shows how to turn both on once email works.
- A rate-limited sign-in. Staff sign-in, its verification step, password reset, setup, invitation acceptance and the members magic-link and one-time-code routes share one budget of 20 requests a minute per address (after a burst of 20), in any spelling of the address. Ghost's own lock-out applies on top. nginx uses the address of the connection, never a header a client sends.
- Safe uploads. Files under
/content/images,/content/filesand/content/mediaare sent withnosniffand a sandboxingcontentsecurity policy. Only passive types (images, PDF, fonts, audio, video) open in the browser. Anything else, SVG and HTML included, is sent as a download. The rest of the content folder is never served. - Staff sessions not usable from other sites. Ghost accepts an admin request that carries neither an
Originnor aRefererheader as if it came from the admin itself. nginx requires same-site proof for every admin API request that carries a staff session, so a link or image on another site cannot run admin actions (such as Ghost's server-side link preview fetch) with a signed-in editor's session. Admin API integrations, which sign requests with a key, are unaffected. - Hardened defaults. Ghost runs as its own
ghostaccount that can write only thecontentfolder; the code, configuration and ghost-cli record are read only to it. MySQL has no network listener (Ghost uses its local socket), itsrootaccount signs in only through the socket as the operating system's root, and its binary and general logs are off. Ghost, nginx, MySQL and the first boot service are blocked from the Azure Instance Metadata Service and the Azure WireServer (the Azure agent and cloud-init keep their access), and requests for a host name the VM does not serve are dropped. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Ghost 6.73.0 with ghost-cli 1.34.0, Node.js 22.23.4, MySQL 8.0, nginx, Ubuntu 24.04 LTS, cloud-init and the Azure Linux agent.
Recommended VM size: Standard_B2s (2 vCPU, 4 GiB) for a typical blog or newsletter, or a D-series size for busy sites.
Prerequisites
- An Azure subscription, an SSH key pair and a virtual network subnet.
- A network security group that allows TCP 22 (SSH), TCP 443 (Ghost) and TCP 80 (redirects to HTTPS) from the networks that should reach them. Keep 22 to your own networks. Open 443 to your readers for a public site, or only to your team while you set it up.
Deploy the VM
In the Azure portal, open the Ghost 6 on Ubuntu 24.04 LTS listing from SecureAnyCloud and choose Create. Pick Standard_B2s, your SSH public key and a network security group as described above, then create the VM. Copy its public IP address from the VM's Overview page.
Alternatively, deploy with the Azure CLI. Replace the values in angle brackets:
az vm create --resource-group <resource-group> --name ghost \
--image <the image URN from the listing> --size Standard_B2s \
--admin-username azureuser --ssh-key-values ~/.ssh/id_rsa.pub \
--public-ip-sku Standard --nsg-rule NONE
az network nsg rule create --resource-group <resource-group> --nsg-name ghostNSG --name ghost-web \
--priority 1000 --access Allow --protocol Tcp --destination-port-ranges 80 443 --source-address-prefixes <your-cidr>Allow a minute or two for first boot after the VM is running. Ghost builds its database on its first start.
Connect to your VM
ssh azureuser@<public-ip>Sign in with the admin username you chose when you created the VM (azureuser in this guide).
Check the services
Confirm that MySQL, Ghost (the ghost_sac service) and nginx are all active:
systemctl is-active mysql ghost_sac nginxactive active activeManage Ghost with ghost-cli from its folder, as your admin user. First boot gives the ghost-cli files to the account you created with the VM.
cd /var/www/ghost && ghost versionGhost-CLI version: 1.34.0 Ghost version: 6.73.0 (at /var/www/ghost)Only SSH, HTTP and HTTPS listen on the network. Ghost listens on the loopback interface behind nginx, and MySQL has no TCP listener at all:
sudo ss -tlnH | awk '{print $4}' | sort -u0.0.0.0:22 0.0.0.0:443 0.0.0.0:80 127.0.0.1:2368 127.0.0.53%lo:53 127.0.0.54:53 [::]:22 [::]:443 [::]:80Get your sign-in details
Show the credentials file. It holds the address Ghost is served as (your VM's public IP address by default), the admin address, the owner's sign-in email and password, and the SHA-256 fingerprint of the VM's certificate. All of these are generated on this VM.
sudo cat /root/ghost-credentials.txt# Ghost on this instance (sac). Root-only (0600). Generated at first boot: 2026-10-28T16:20:41Z GHOST_URL=https://<public-ip>/ GHOST_ADMIN_URL=https://<public-ip>/ghost/ GHOST_OWNER_EMAIL=owner@example.invalid GHOST_OWNER_PASSWORD=<GHOST_OWNER_PASSWORD> ... GHOST_TLS_SHA256=<64 hex digits separated by colons>The owner signs in with the email owner@example.invalid, an address that can never receive mail. Once you are signed in, change it to your own address under Settings > Staff (Step 5).
Visit the site and sign in
Open GHOST_URL in a browser. The certificate is self-signed, so the browser warns once. Before you continue, compare the certificate's SHA-256 fingerprint in the browser's certificate viewer with GHOST_TLS_SHA256 from Step 4. If they match, you are talking to your own VM.
Do not add this certificate to your computer's trusted certificates. Step 11 replaces it with one from a certificate authority. Visiting the VM by another of its addresses redirects to the configured one.
Open GHOST_ADMIN_URL and sign in with the owner email and password. Ghost's admin opens with Posts, Pages, Tags, Members and Settings. Start with these settings:
- Settings > Staff: open your own profile, set your name and your real email address, and change the password if you like.
- Settings > General: set the site title, description and time zone.
- Settings > Design & branding: choose and customize a theme (Source is active).
Write a post with Posts > New post, then select Publish. It appears on the home page at once.
Check HTTPS and the setup state
Confirm that port 80 redirects to HTTPS on the configured address:
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://127.0.0.1/301 https://<public-ip>/Confirm that the setup API reports setup as complete and refuses to run again, so nobody can take over the site:
H=$(sed -n 's/^set \$ghost_host "\(.*\)";$/\1/p' /etc/nginx/ghost-host.conf)
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/ghost/api/admin/authentication/setup/"; echo
curl -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w 'second setup: %{http_code}\n' -H 'Content-Type: application/json' \
-d '{"setup":[{"name":"x","email":"x@example.invalid","password":"Xx1234567890abc","blogTitle":"x"}]}' \
"https://$H/ghost/api/admin/authentication/setup/"{"setup":[{"status":true}]} second setup: 403Your data
Ghost keeps its content in two places: the MySQL database ghost_prod (posts, members, settings and keys) and the folder /var/www/ghost/content (uploaded images, files and media, and your themes). Back up both, for example with an Azure Backup policy or a snapshot of the VM's OS disk. You can also back up from the command line:
sudo mysqldump --single-transaction ghost_prod | gzip > ghost_prod-$(date +%F).sql.gz
sudo tar -C /var/www/ghost -czf ghost-content-$(date +%F).tar.gz contentGhost's own export (Settings > Advanced > Migration tools) saves posts and settings as JSON. Keep backups private. The database holds your members' email addresses and the keys that sign sessions.
E-mail, members sign-up and device verification
Ghost sends staff password resets, member sign-in links and newsletters by email. The image does not configure a mail service, so set one up first. For transactional mail, add an SMTP server (for example Azure Communication Services Email, SendGrid or Mailgun) to /var/www/ghost/config.production.json, replacing the "mail" block:
"mail": { "transport": "SMTP", "from": "Your Site <noreply@your-domain>", "options": { "host": "<smtp-host>", "port": 587, "secure": false, "auth": { "user": "<smtp-user>", "pass": "<smtp-password>" } } }Azure blocks outbound TCP 25 for most subscriptions, so use port 587. Then restart Ghost and test a password reset:
cd /var/www/ghost && ghost restartNewsletters use Mailgun, which you set up under Settings > Newsletters. Ghost's own documentation explains how.
Once email works, you can change these two defaults:
- Members sign-up: Settings > Membership > Access, Subscription access, is set to Invite-only. Choose Anyone can sign up to open it.
- Staff device verification (an emailed code when staff sign in from a new browser) is off. To turn it on, make sure every staff account has a real email address, add
"security": { "staffDeviceVerification": true }toconfig.production.json(replace the existingsecurityblock) and runghost restart. Leave it off without working mail, or staff are locked out.
The closed-by-default settings
These settings are in /var/www/ghost/config.production.json. The file holds no password, so ghost-cli can manage it.
Setting Image value To restore Ghost's default
privacy.useTinfoil true: no Gravatar, structured data or IndexNow pings remove the privacy block
updateCheck.url a closed local port: no daily update check remove the updateCheck block (Ghost sends only its version and shows release and security notices in the admin)
portal, sodoSearch, comments, announcementBar, signupForm, adminToolbar served from /sac-apps/ on your VM remove the blocks to load them from jsDelivr
These settings are in the database. Change them in the admin, where the settings search box finds each one. The social web (Settings > Growth > Network), Unsplash and Pintura (Settings > Advanced > Integrations) and web analytics (Settings > General settings > Analytics) are all off. After you edit the configuration, run ghost restart.
The front-end scripts the image serves, and their versions:
cat /usr/local/share/sac/ghost-apps/VERSIONSportal 2.72.8 sodo-search 1.9.6 announcement-bar 1.1.679 comments-ui 1.6.448 signup-form 0.4.3 admin-toolbar 0.1.663Webmentions. Receiving them is refused by nginx. To accept them, remove the if ($ghost_path_refused) line from /etc/nginx/sites-available/sac-ghost (or drop the webmentions line from the map in /etc/nginx/conf.d/sac-ghost-guard.conf) and run sudo nginx -t && sudo systemctl reload nginx. Ghost refuses to fetch private network addresses either way.
Updating Ghost
Take a snapshot or backup first. Then update Ghost in place with ghost-cli, as your admin user:
cd /var/www/ghost && ghost updateFront-end scripts. After an update, compare the versions in /var/www/ghost/current/core/shared/config/defaults.json (portal.version and the others) with /usr/local/share/sac/ghost-apps/VERSIONS. If Ghost now asks for a newer line (for example portal 2.73), either remove the matching block from config.production.json (Step 9) or place that release's umd files in the folder.
Keep Node.js up to date with sudo apt-get update && sudo apt-get upgrade (the NodeSource repository is configured).
Your own DNS name and certificate
Point a DNS record at the VM's public IP address, then set the name Ghost is served as. This mints a new self-signed certificate for the name, pins it in nginx (the VM's IP addresses redirect to it), sets Ghost's url, restarts Ghost, updates /root/ghost-credentials.txt and keeps the name across reboots:
sudo ghost-sac-set-hostname <your-domain>Then replace the self-signed certificate with one issued by a certificate authority for your name (for example with certbot, or one from your organization), copy it over the generated pair and reload nginx (running ghost-sac-set-hostname again replaces it with a self-signed one):
sudo install -m 0644 <your-domain>.crt /etc/nginx/ssl/ghost.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/ssl/ghost.key
sudo nginx -t && sudo systemctl reload nginxOnce browsers trust the certificate you can also add an HSTS header to /etc/nginx/snippets/sac-ghost-headers.conf. The image does not send one: browsers ignore HSTS from a site whose certificate they do not trust.
Behind Azure Front Door or Application Gateway: terminate TLS on it and forward to port 443 on the VM with the configured host name. nginx always uses the connection's address as the client address, so with a proxy in front every request appears to come from the proxy; Ghost's per-address limits then apply to the proxy as a whole.
The VM's public IP address is also added to its loopback interface at every boot (ghost-selfroute.service), so when Ghost reads its own images (it does this to size the images on a page) the request stays on the VM instead of leaving through Azure's network address translation.
Security checks
The owner password signs in, a wrong one is refused with Ghost's own error, and the checks below show the defaults in force:
sudo ghost-sac-cred-roundtripOK https 201 200 422H=$(sed -n 's/^set \$ghost_host "\(.*\)";$/\1/p' /etc/nginx/ghost-host.conf) R=(curl -sk --resolve "$H:443:127.0.0.1" -o /dev/null) "${R[@]}" -w 'webmention %{http_code}\n' -d 'source=https://example.com/&target=https://example.org/' "https://$H/webmentions/receive/" "${R[@]}" -w 'upper-case upload path %{http_code}\n' "https://$H/CONTENT/images/x.svg" "${R[@]}" -w 'unknown host %{http_code}\n' -H 'Host: other.example' https://127.0.0.1/ ; echowebmention 404 upper-case upload path 404 unknown host 000(000 means nginx closed the connection without an answer.) The certificate is an end-entity certificate for this VM, not a certificate authority:
sudo openssl x509 -in /etc/nginx/ssl/ghost.crt -noout -ext basicConstraints,keyUsage,extendedKeyUsageX509v3 Basic Constraints: critical CA:FALSE X509v3 Key Usage: critical Digital Signature, Key Encipherment X509v3 Extended Key Usage: TLS Web Server AuthenticationGhost, nginx and MySQL are blocked from the Azure Instance Metadata Service and the WireServer:
for u in ghost_sac nginx mysql; do printf '%s: ' $u; systemctl show -p IPAddressDeny --value $u | tr ' ' '\n' | sort | tr '\n' ' '; echo; doneghost_sac: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128 nginx: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128 mysql: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128The credentials and the database password are readable by root only, Ghost's configuration holds no secret, the content folder belongs to Ghost and the code to your admin user, who runs ghost-cli:
sudo stat -c '%a %U %n' /root/ghost-credentials.txt /etc/ghost-sac/db-password /var/www/ghost/content600 root /root/ghost-credentials.txt 600 root /etc/ghost-sac/db-password 775 ghost /var/www/ghost/contentPasswords
ghost-sac-set-password gives the owner a new random password with Ghost's own password hashing, signs out every staff session, clears sign-in lock-outs and records the password in /root/ghost-credentials.txt. Run it without arguments, or pass your own password (12 to 96 letters and digits) on standard input. Staff change their own password under Settings > Staff.
sudo ghost-sac-set-password printf '%s\n' '<your-new-password>' | sudo ghost-sac-set-password --stdinServer components
Component Path
Ghost (ghost-cli instance SecureAnyCloud, owned by your admin user) /var/www/ghost
Configuration (no secrets) /var/www/ghost/config.production.json
Uploads and themes (owned by ghost) /var/www/ghost/content
Ghost service /lib/systemd/system/ghost_sac.service
Database password (root only, a systemd credential for Ghost) /etc/ghost-sac/db-password
Database (MySQL datadir) and settings /var/lib/mysql, /etc/mysql/mysql.conf.d/99-sac-ghost.cnf
nginx site, request limits and cross-site rules /etc/nginx/sites-available/sac-ghost, /etc/nginx/conf.d/sac-ghost-guard.conf, /etc/nginx/conf.d/sac-ghost-xsite.conf, /etc/nginx/conf.d/sac-xsite-guard.conf, /etc/nginx/snippets/sac-ghost-*.conf
Front-end scripts served locally /usr/local/share/sac/ghost-apps/
Certificate and key /etc/nginx/ssl/
The VM's addresses from Azure metadata (every boot) ghost-address.service, /run/sac-ghost/address.env
First boot and helpers /usr/local/sbin/ghost-firstboot, ghost-sac-set-hostname, ghost-sac-set-password, ghost-sac-cred-roundtrip
Licence, release and image notes /usr/share/doc/ghost-sac/
Credentials /root/ghost-credentials.txt (mode 0600)
Security recommendations
- Allow port 22 in the network security group from your own networks only.
- Set your real email on the owner account and keep its password secret (Step 13). Give each writer their own staff account with the least role they need (Contributor, Author, Editor).
- Configure email (Step 8), then consider turning on staff device verification.
- Use your own DNS name and a certificate from a certificate authority (Step 11).
- Keep Ghost on the newest release (Step 10) and install only themes you trust.
- Keep the operating system patched. Unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else (Node.js included).
If you deployed before 28 October 2026
Images published before this date ran Ghost 6.36.0. That version is affected by published Ghost security advisories (among them GHSA-62q6-4hv4-vjrw, rated critical). Those images also opened Ghost's setup wizard on plain HTTP, so until someone completed it, the first visitor to /ghost/ could create the site owner. To check a VM from such an image, run:
grep '"version"' /var/www/ghost/current/package.json
sudo grep '^MYSQL_ROOT_PASSWORD=' /stage/scripts/ghost-credentials.log mysql -u root -p ghost_prod -e 'select email, status, created_at from users'The mysql command asks for the password. Paste the MYSQL_ROOT_PASSWORD value that the second command shows.
If the version is older than 6.73.0, or there is a staff account you did not create, deploy a new VM from the current image. Move your content to it with Ghost's export and import (Settings > Advanced > Migration tools) and a copy of /var/www/ghost/content/images, then delete the old VM. Change the passwords of every staff account on the new site.
Support and licensing
Ghost is free software under the MIT license (a copy is at /usr/share/doc/ghost-sac/LICENSE.ghost). The bundled libraries carry their own open source licenses. There is no per-user or per-CPU license fee.
This image installs the unmodified upstream release, plus the SecureAnyCloud first-boot setup, settings and helpers described above. The image price covers SecureAnyCloud's packaging, hardening, first-boot setup and support. Ghost is a trademark of the Ghost Foundation. SecureAnyCloud is not affiliated with or endorsed by it.
Email: sales@secureanycloud.com
Deploy on Azure
Launch Ghost 6 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com