< the skull

GHOST CMS ON DEBIAN 12: WHAT WE TRIED, WHAT FAILED, AND WHAT FINALLY WORKED
===========================================================================
2026-04-15

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 `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
----------

  * Ghost install guide: https://docs.ghost.org/install/ubuntu [1]
  * Ghost-CLI docs: https://docs.ghost.org/ghost-cli [2]
  * Ghost configuration docs: https://docs.ghost.org/config [3]
  * Ghost supported Node versions: https://docs.ghost.org/faq/node-versions [4]
  * Ghost start troubleshooting:
    https://docs.ghost.org/faq/errors-running-ghost-start [5]
  * npm global folder layout:
    https://docs.npmjs.com/cli/v11/configuring-npm/folders [6]

*

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.

Links:
  [1] https://docs.ghost.org/install/ubuntu
  [2] https://docs.ghost.org/ghost-cli
  [3] https://docs.ghost.org/config
  [4] https://docs.ghost.org/faq/node-versions
  [5] https://docs.ghost.org/faq/errors-running-ghost-start
  [6] https://docs.npmjs.com/cli/v11/configuring-npm/folders


< the skull