Ghost 6.72.0 on AWS User Guide
Overview
This guide covers launching and using the Ghost 6.72.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg BookStack, CMS, Blogs and Wikis listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Ghost is a free and open source publishing platform written in Node.js: 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.
The cloudimg image adds:
- 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. Your instance's first boot initialises its own MySQL, creates Ghost's database user with a password generated on your instance, runs Ghost once on a random local port so its migrations generate this site's session secrets, member and staff signing keys and API keys, and creates the owner account through Ghost's own setup API over the local loopback interface, with a password generated on your instance. It then 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. - Secrets of your instance's own, never on a command line. The owner password, the database password, Ghost's keys and the TLS key are generated on your instance; 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.72.0 is checked file for file against the official npm release, pinned by its checksum. Node.js 22.23.4 (the October 2026 security release) comes from NodeSource's signed repository.
- A mitigation for an unfixed Ghost dependency. Ghost 6.72.0 ships body-parser 1.20.8, which an unauthenticated request with a compressed body can crash (GHSA-27r3-jxmh-p828). nginx refuses every request that carries a
Content-Encodingheader (HTTP 415; browsers never compress request bodies), and systemd restarts Ghost within two seconds if it ever stops. The mitigation can be lifted once you update to a Ghost release with body-parser 1.20.9 or later (Step 10). - HTTPS from the first boot. nginx serves Ghost on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS. Ghost's own address is pinned to your host name, so every link, e-mails 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 server (pinned releases) instead of from jsDelivr, and the default cover images Ghost points at static.ghost.org are removed. With the image's settings, Ghost made no outbound connection while it started, served pages and published a post, and the public site's pages load nothing from other servers. 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 e-mail. Ghost's members sign in with e-mailed links, and the image has no mail service. Sign-up is set to invite-only, and staff device verification (an e-mailed 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 e-mail 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 sandboxing content security policy; only passive types (images, PDF, fonts, audio, video) open in the browser and anything else, SVG and HTML included, is sent as a download. The rest of thecontentfolder 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 instance metadata service, the AMI requires IMDSv2, and requests for a host name the instance 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.72.0 with ghost-cli 1.34.0, Node.js 22.23.4, MySQL 8.0, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
- A 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.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Ghost 6.72.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=ghost}]'
First boot takes about a minute or two after the instance is running (Ghost builds its database on the first start).
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Ghost 6.72.0 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key>.pem ubuntu@<public-ip-or-dns>
Step 3: Check the services
MySQL, Ghost (the ghost_cloudimg service) and nginx should all be active:
systemctl is-active mysql ghost_cloudimg nginx
active
active
active
Ghost is managed with ghost-cli from its folder, as the ubuntu user:
cd /var/www/ghost && ghost version
Ghost-CLI version: 1.34.0
Ghost version: 6.72.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 -u
0.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
[::]:80
Step 4: Get your sign-in details
The credentials file holds the address Ghost is served as (your instance's public DNS name by default), the admin address and the owner's sign-in e-mail and password, all generated on this instance. Show it with:
sudo cat /root/ghost-credentials.txt
# Ghost on this instance (cloudimg). Root-only (0600). Generated at first boot: 2026-10-28T00:12:25Z
GHOST_URL=https://ec2-52-201-222-176.compute-1.amazonaws.com/
GHOST_ADMIN_URL=https://ec2-52-201-222-176.compute-1.amazonaws.com/ghost/
GHOST_OWNER_EMAIL=owner@example.invalid
GHOST_OWNER_PASSWORD=<GHOST_OWNER_PASSWORD>
The owner signs in with the e-mail owner@example.invalid (a placeholder that can never receive mail). Change it to your own address under Settings > Staff once you are in (Step 5).
Step 5: Visit the site and sign in
Open GHOST_URL in a browser. The certificate is self signed, so the browser warns once; continue to the site (Step 11 replaces it with your own). Visiting the instance by its IP address redirects to the configured name.


Open GHOST_ADMIN_URL, sign in with the owner e-mail and password, and you are in Ghost's admin: Posts, Pages, Tags, Members and Settings. First:
- Settings > Staff: open your own profile, set your name and your real e-mail address, and change the password if you like.
- Settings > General: set the site title, description and time zone.
- Settings > Design & branding: choose and customise a theme (Source is active).

Write a post with Posts > New post, then Publish. It appears on the home page at once.


Step 6: Check HTTPS and the setup state
Port 80 redirects to HTTPS on the configured name:
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://127.0.0.1/
301 https://ec2-52-201-222-176.compute-1.amazonaws.com/
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: 403
Step 7: Your 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 a snapshot of the instance's EBS volume, or:
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 content
Ghost's own export (Settings > Advanced > Migration tools) saves posts and settings as JSON. Keep backups private: the database holds your members' e-mail addresses and the keys that sign sessions.
Step 8: E-mail, members sign-up and device verification
Ghost sends staff password resets, member sign-in links and newsletters by e-mail. The image does not configure a mail service, so set one up first. For transactional mail, add an SMTP server (for example Amazon SES) to /var/www/ghost/config.production.json, replacing the "mail" block:
"mail": {
"transport": "SMTP",
"from": "Your Site <noreply@your-domain>",
"options": {
"host": "email-smtp.<region>.amazonaws.com",
"port": 587,
"secure": false,
"auth": { "user": "<smtp-user>", "pass": "<smtp-password>" }
}
}
Then restart Ghost and test a password reset:
cd /var/www/ghost && ghost restart
Newsletters use Mailgun, set under Settings > Newsletters (Ghost's own documentation explains it).
Once e-mail works:
- Members sign-up: Settings > Membership > Access, Subscription access, is Invite-only. Choose Anyone can sign up to open it.
- Staff device verification (an e-mailed code when staff sign in from a new browser) is off. To turn it on, make sure every staff account has a real e-mail 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.
Step 9: The closed-by-default settings
These are in /var/www/ghost/config.production.json (it 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 /cloudimg-apps/ on your server |
remove the blocks to load them from jsDelivr |
In the database (change them in the admin; the settings search box finds each): the social web (Settings > Growth > Network), Unsplash and Pintura (Settings > Advanced > Integrations) and web analytics (Settings > General settings > Analytics) are off. Run ghost restart after editing the configuration.
The front-end scripts the image serves, and their versions:
cat /usr/local/share/cloudimg/ghost-apps/VERSIONS
portal 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.663
Webmentions. Receiving them is refused by nginx. To accept them, remove the if ($ghost_path_refused) line from /etc/nginx/sites-available/cloudimg-ghost (or drop the webmentions line from the map in /etc/nginx/conf.d/cloudimg-ghost-guard.conf) and run sudo nginx -t && sudo systemctl reload nginx. Ghost refuses to fetch private network addresses either way.
Step 10: Updating Ghost
ghost-cli updates Ghost in place, as the ubuntu user, after a snapshot or backup:
cd /var/www/ghost && ghost update
The compressed-body mitigation. nginx keeps refusing request bodies with a Content-Encoding header (415) after an update. Once ghost version reports a release whose body-parser is 1.20.9 or later (check with grep '"version"' /var/www/ghost/current/node_modules/body-parser/package.json), you may remove the if ($http_content_encoding) { return 415; } lines from /etc/nginx/sites-available/cloudimg-ghost and reload nginx. Keeping them does no harm.
Front-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/cloudimg/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).
Step 11: Your own DNS name and certificate
Point a DNS record at the instance, then set the name Ghost is served as. This mints a new self signed certificate for the name, pins it in nginx (the instance'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-cloudimg-set-hostname <your-domain>
To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running ghost-cloudimg-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 nginx
Once browsers trust the certificate you can also add an HSTS header to /etc/nginx/snippets/cloudimg-ghost-headers.conf. The image does not send one: browsers ignore HSTS from a site whose certificate they do not trust.
Behind a load balancer or CloudFront: terminate TLS on it and forward to port 443 on the instance 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.
Step 12: 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-cloudimg-cred-roundtrip
OK https 201 200 422
H=$(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 'compressed body %{http_code}\n' -H 'Content-Encoding: gzip' --data-binary x "https://$H/ghost/api/admin/session/"
"${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/ ; echo
compressed body 415
webmention 404
upper-case upload path 404
unknown host 000
(000 means nginx closed the connection without an answer.) Ghost, nginx and MySQL are blocked from the instance metadata service:
systemctl show -p IPAddressDeny ghost_cloudimg nginx mysql
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
The 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 the ubuntu user who runs ghost-cli:
sudo stat -c '%a %U:%G %n' /root/ghost-credentials.txt /etc/ghost-cloudimg/db-password /var/www/ghost/config.production.json /var/www/ghost/content /var/www/ghost/current/core
600 root:root /root/ghost-credentials.txt
600 root:root /etc/ghost-cloudimg/db-password
664 ubuntu:ubuntu /var/www/ghost/config.production.json
775 ghost:ghost /var/www/ghost/content
755 ubuntu:ubuntu /var/www/ghost/current/core
Step 13: Passwords
ghost-cloudimg-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-cloudimg-set-password
printf '%s\n' '<your-new-password>' | sudo ghost-cloudimg-set-password --stdin
Step 14: Server components
| Component | Path |
|---|---|
Ghost (ghost-cli instance cloudimg, owned by ubuntu) |
/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_cloudimg.service |
| Database password (root only, a systemd credential for Ghost) | /etc/ghost-cloudimg/db-password |
| Database (MySQL datadir) and settings | /var/lib/mysql, /etc/mysql/mysql.conf.d/99-cloudimg-ghost.cnf |
| nginx site, request limits and cross-site rules | /etc/nginx/sites-available/cloudimg-ghost, /etc/nginx/conf.d/cloudimg-ghost-guard.conf, /etc/nginx/conf.d/cloudimg-ghost-xsite.conf, /etc/nginx/conf.d/cloudimg-xsite-guard.conf, /etc/nginx/snippets/cloudimg-ghost-*.conf |
| Front-end scripts served locally | /usr/local/share/cloudimg/ghost-apps/ |
| Certificate and key | /etc/nginx/ssl/ |
| First boot and helpers | /usr/local/sbin/ghost-firstboot, ghost-cloudimg-set-hostname, ghost-cloudimg-set-password, ghost-cloudimg-cred-roundtrip |
| Licence, release and image notes | /usr/share/doc/ghost-cloudimg/ |
| Credentials | /root/ghost-credentials.txt (mode 0600) |
Step 15: Security recommendations
- Allow port 22 in the security group from your own networks only.
- Set your real e-mail 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 e-mail (Step 8), then consider turning on staff device verification.
- Use your own DNS name and a certificate your browsers trust (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).
Support and licensing
Ghost is free software under the MIT licence (a copy is at /usr/share/doc/ghost-cloudimg/LICENSE.ghost); the bundled libraries carry their own open source licences. There is no per user or per CPU licence fee. This image installs the unmodified upstream release, plus the cloudimg first boot, settings and helpers described above. Ghost is a trademark of the Ghost Foundation; cloudimg is not affiliated with or endorsed by it. The AMI price covers cloudimg's hardening, first boot setup and support.
- Email: support@cloudimg.co.uk