Ghost CMS on Debian 12: what we tried, what failed, and what finally worked
Disclaimer
Ghost officially recommends a production stack centered on Ubuntu, Ghost-CLI, systemd, Node.js 22 LTS, MySQL 8, and NGINX. This guide documents a Debian 12 deployment that eventually worked, but it required workarounds beyond the standard supported path.
This post uses placeholders instead of real values:
<your-domain><your-linux-user><your-db-name><your-db-user><your-db-password><your-service-name>
Summary
This Debian 12 install did not succeed cleanly through the default Ghost-CLI runtime flow.
What eventually worked was:
- Install Ghost with
ghost install - Keep the site under
/var/www/<your-domain> - Use Node.js 22
- Use MySQL 8
- Skip Ghost-managed MySQL, NGINX, and SSL if those are already configured
- Replace the Ghost-CLI-generated runtime service with a manual systemd unit
- Fix execute permissions for the bundled
esbuildbinary
That was the actual fix.
What failed first
1) A bad npm global prefix
A user-level npm prefix can quietly break Ghost-CLI behavior by placing global binaries under a home directory instead of a clean global path.
Check this:
cat ~/.npmrc
If you see something like:
prefix=/home/<your-linux-user>/.local
move it away:
mv ~/.npmrc ~/.npmrc.bak
That prevents Ghost-CLI from being tied to a user-local path.
2) Installing Ghost under the home directory
Installing under a user home caused permission/readability problems for production use.
Use a dedicated production path instead:
/var/www/<your-domain>
3) Relying on the Ghost-CLI-generated runtime on this Debian 12 host
Ghost-CLI successfully unpacked Ghost, configured the instance, and even created a systemd unit, but startup failed repeatedly. The install phase completed much further than the runtime phase.
The result was a recurring pattern:
Could not communicate with Ghostghost runfailing under the managed service path- runtime failures before the site stayed up
4) Runtime failure caused by esbuild permissions
After bypassing the Ghost-CLI-managed service and starting Ghost directly with Node, the application booted much further:
- database connected
- site booted
- Ghost began startup
- then crashed on
esbuildwithEACCES
That turned out to be the final real blocker.
Final working approach
The working setup on Debian 12 was:
- Ghost code installed under
/var/www/<your-domain> - Ghost unpacked/configured by Ghost-CLI
- Ghost not started by the Ghost-CLI-generated service
- a manual
systemdservice running:
/usr/bin/node /var/www/<your-domain>/current/index.js
- site owned by
<your-linux-user> - execute permission restored for
esbuild
Prerequisites
DNS
Point your domain to the server before you finish the setup:
Arecord for<your-domain>→ your server IP- optional
CNAMEor redirect forwww
Existing services assumed in this guide
This guide assumes you already have:
- SSL already handled outside Ghost
- NGINX already installed
- MySQL already installed
- a database and database user already prepared
If not, use the official Ghost install documentation for the standard path first.
1) Install Node.js 22
Check your version:
node -v
npm -v
Ghost currently requires Node.js 22 LTS.
If needed, install Node 22:
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
2) Install Ghost-CLI globally
Remove any broken local/user-level installation first:
rm -f ~/.local/bin/ghost
rm -rf ~/.local/lib/node_modules/ghost-cli
mv ~/.npmrc ~/.npmrc.bak 2>/dev/null || true
Install globally:
sudo npm install -g ghost-cli@latest
If the command exists but is not executable because of permissions under /usr/local/lib/node_modules, fix it:
sudo chmod 755 /usr/local
sudo chmod 755 /usr/local/lib
sudo chmod 755 /usr/local/lib/node_modules
sudo chown -R root:root /usr/local/lib/node_modules/ghost-cli
sudo chmod -R a+rX /usr/local/lib/node_modules/ghost-cli
sudo ln -sf /usr/local/lib/node_modules/ghost-cli/bin/ghost /usr/local/bin/ghost
hash -r
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"
Verify:
command -v ghost
ghost --version
3) Prepare the install directory
sudo rm -rf /var/www/<your-domain>
sudo mkdir -p /var/www/<your-domain>
sudo chown <your-linux-user>:<your-linux-user> /var/www/<your-domain>
sudo chmod 775 /var/www/<your-domain>
cd /var/www/<your-domain>
4) Prepare the MySQL database
If this is a clean deployment and you do not need existing content, recreate the database:
mysql -u <your-db-user> -p -e "DROP DATABASE IF EXISTS <your-db-name>; CREATE DATABASE <your-db-name> CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
5) Run ghost install
This example assumes MySQL, NGINX, and SSL are already handled externally:
ghost install \
--url https://<your-domain> \
--ip 127.0.0.1 \
--port 2589 \
--db mysql \
--dbhost 127.0.0.1 \
--dbuser <your-db-user> \
--dbpass '<your-db-password>' \
--dbname <your-db-name> \
--process systemd \
--no-setup-mysql \
--no-setup-nginx \
--no-setup-ssl
At this point, Ghost-CLI may still fail to keep the site alive on Debian 12 even though installation mostly succeeds.
6) Disable the Ghost-CLI-generated service
If Ghost-CLI creates a service that fails on runtime, disable it:
sudo systemctl stop ghost_<your-domain-as-name> 2>/dev/null || true
sudo systemctl disable ghost_<your-domain-as-name> 2>/dev/null || true
sudo rm -f /lib/systemd/system/ghost_<your-domain-as-name>.service
sudo systemctl daemon-reload
sudo systemctl reset-failed
Use the actual generated unit name on your server.
7) Normalize ownership and permissions for the site
Make the site tree consistently owned by your runtime user:
sudo chown -R <your-linux-user>:<your-linux-user> /var/www/<your-domain>
sudo find /var/www/<your-domain> -type d -exec chmod 755 {} \;
sudo find /var/www/<your-domain> -type f -exec chmod 644 {} \;
sudo chmod 775 /var/www/<your-domain>/content
sudo find /var/www/<your-domain>/content -type d -exec chmod 775 {} \;
8) Create a manual systemd service
Create /etc/systemd/system/ghost-manual.service:
[Unit]
Description=Ghost manual service
After=network.target mysql.service
Wants=network.target
[Service]
Type=simple
User=<your-linux-user>
Group=<your-linux-user>
WorkingDirectory=/var/www/<your-domain>
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /var/www/<your-domain>/current/index.js
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Create it with:
sudo tee /etc/systemd/system/ghost-manual.service >/dev/null <<'EOF'
[Unit]
Description=Ghost manual service
After=network.target mysql.service
Wants=network.target
[Service]
Type=simple
User=<your-linux-user>
Group=<your-linux-user>
WorkingDirectory=/var/www/<your-domain>
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /var/www/<your-domain>/current/index.js
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
Enable it:
sudo systemctl daemon-reload
sudo systemctl enable --now ghost-manual
Check it:
sudo systemctl status ghost-manual --no-pager -l
sudo journalctl -u ghost-manual -n 100 --no-pager -l
9) Fix the esbuild permission problem
This was the final blocker in the Debian 12 install documented here.
Ghost would start, connect to MySQL, begin initialization, and then crash with an EACCES error when trying to execute esbuild.
Fix it with:
sudo find /var/www/<your-domain> -type f -name esbuild -exec chmod +x {} \;
Or target the common path directly:
sudo chmod +x /var/www/<your-domain>/versions/*/node_modules/.pnpm/@esbuild*/node_modules/@esbuild/*/bin/esbuild
Restart the service:
sudo systemctl restart ghost-manual
sudo systemctl status ghost-manual --no-pager -l
sudo journalctl -u ghost-manual -n 100 --no-pager -l
If the service stays up after this, Ghost is effectively running.
10) Verify Ghost is healthy
Good signs in logs include:
Ghost is running in production...Your site is now available on https://<your-domain>/Database is in a ready state.Ghost database ready ...
Admin URL:
https://<your-domain>/ghost
11) Example NGINX reverse proxy block
If SSL is already configured and you just need to proxy to Ghost on 127.0.0.1:2589, use this in your HTTPS site config:
location / {
proxy_pass http://127.0.0.1:2589;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
Test and reload:
sudo nginx -t
sudo systemctl reload nginx
What did not work reliably in this Debian 12 case
These paths either failed or were unstable in this deployment:
- leaving npm globals under a user-local prefix
- keeping Ghost under a home directory
- relying on the Ghost-CLI-managed runtime service on this Debian 12 host
- ignoring executable permissions inside the Ghost dependency tree
- treating
Could not communicate with Ghostas the real cause instead of a generic wrapper error
What actually worked
This is the short version:
- install Ghost with Ghost-CLI
- keep the install under
/var/www/<your-domain> - use Node 22
- use MySQL 8
- skip Ghost-managed NGINX/SSL/MySQL if they already exist
- replace the generated runtime service with a manual systemd unit
- fix
esbuildexecute permission
That combination worked.
Post-install tasks
Configure mail
If Ghost warns about missing mail configuration, add a proper SMTP setup in config.production.json.
Rotate secrets
If you typed passwords directly into shell history during troubleshooting, rotate them.
Backups
Back up:
/var/www/<your-domain>/content- your MySQL database
Updates
Because this Debian setup uses a manual service workaround, test upgrades carefully before applying them on production.
References
- Ghost install guide: https://docs.ghost.org/install/ubuntu
- Ghost-CLI docs: https://docs.ghost.org/ghost-cli
- Ghost configuration docs: https://docs.ghost.org/config
- Ghost supported Node versions: https://docs.ghost.org/faq/node-versions
- Ghost start troubleshooting: https://docs.ghost.org/faq/errors-running-ghost-start
- npm global folder layout: https://docs.npmjs.com/cli/v11/configuring-npm/folders
Post notes
If you want the least painful path, use Ubuntu and follow the official Ghost installation guide.
If Debian 12 is mandatory, the procedure above is the route that actually worked in this case.