7 min read

Ghost CMS on Debian 12: what we tried, what failed, and what finally worked

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.
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:

  1. Install Ghost with ghost install
  2. Keep the site under /var/www/<your-domain>
  3. Use Node.js 22
  4. Use MySQL 8
  5. Skip Ghost-managed MySQL, NGINX, and SSL if those are already configured
  6. Replace the Ghost-CLI-generated runtime service with a manual systemd unit
  7. Fix execute permissions for the bundled esbuild binary

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 Ghost
  • ghost run failing 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 esbuild with EACCES

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 systemd service 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:

  • A record for <your-domain> → your server IP
  • optional CNAME or redirect for www

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 Ghost as 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 esbuild execute 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


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.