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

· PABLO'S DEVLOG


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:


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:

1cat ~/.npmrc

If you see something like:

1prefix=/home/<your-linux-user>/.local

move it away:

1mv ~/.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:

1/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:


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:

That turned out to be the final real blocker.


Final working approach #

The working setup on Debian 12 was:

1/usr/bin/node /var/www/<your-domain>/current/index.js

Prerequisites #

DNS #

Point your domain to the server before you finish the setup:

Existing services assumed in this guide #

This guide assumes you already have:

If not, use the official Ghost install documentation for the standard path first.


1) Install Node.js 22 #

Check your version:

1node -v
2npm -v

Ghost currently requires Node.js 22 LTS.

If needed, install Node 22:

1sudo apt-get update
2sudo apt-get install -y ca-certificates curl gnupg
3sudo mkdir -p /etc/apt/keyrings
4curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
5NODE_MAJOR=22
6echo "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
7sudo apt-get update
8sudo apt-get install -y nodejs

2) Install Ghost-CLI globally #

Remove any broken local/user-level installation first:

1rm -f ~/.local/bin/ghost
2rm -rf ~/.local/lib/node_modules/ghost-cli
3mv ~/.npmrc ~/.npmrc.bak 2>/dev/null || true

Install globally:

1sudo 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:

1sudo chmod 755 /usr/local
2sudo chmod 755 /usr/local/lib
3sudo chmod 755 /usr/local/lib/node_modules
4sudo chown -R root:root /usr/local/lib/node_modules/ghost-cli
5sudo chmod -R a+rX /usr/local/lib/node_modules/ghost-cli
6sudo ln -sf /usr/local/lib/node_modules/ghost-cli/bin/ghost /usr/local/bin/ghost
7hash -r
8export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"

Verify:

1command -v ghost
2ghost --version

3) Prepare the install directory #

1sudo rm -rf /var/www/<your-domain>
2sudo mkdir -p /var/www/<your-domain>
3sudo chown <your-linux-user>:<your-linux-user> /var/www/<your-domain>
4sudo chmod 775 /var/www/<your-domain>
5cd /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:

1mysql -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:

 1ghost install \
 2  --url https://<your-domain> \
 3  --ip 127.0.0.1 \
 4  --port 2589 \
 5  --db mysql \
 6  --dbhost 127.0.0.1 \
 7  --dbuser <your-db-user> \
 8  --dbpass '<your-db-password>' \
 9  --dbname <your-db-name> \
10  --process systemd \
11  --no-setup-mysql \
12  --no-setup-nginx \
13  --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:

1sudo systemctl stop ghost_<your-domain-as-name> 2>/dev/null || true
2sudo systemctl disable ghost_<your-domain-as-name> 2>/dev/null || true
3sudo rm -f /lib/systemd/system/ghost_<your-domain-as-name>.service
4sudo systemctl daemon-reload
5sudo 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:

1sudo chown -R <your-linux-user>:<your-linux-user> /var/www/<your-domain>
2sudo find /var/www/<your-domain> -type d -exec chmod 755 {} \;
3sudo find /var/www/<your-domain> -type f -exec chmod 644 {} \;
4sudo chmod 775 /var/www/<your-domain>/content
5sudo find /var/www/<your-domain>/content -type d -exec chmod 775 {} \;

8) Create a manual systemd service #

Create /etc/systemd/system/ghost-manual.service:

 1[Unit]
 2Description=Ghost manual service
 3After=network.target mysql.service
 4Wants=network.target
 5
 6[Service]
 7Type=simple
 8User=<your-linux-user>
 9Group=<your-linux-user>
10WorkingDirectory=/var/www/<your-domain>
11Environment=NODE_ENV=production
12ExecStart=/usr/bin/node /var/www/<your-domain>/current/index.js
13Restart=always
14RestartSec=5
15
16[Install]
17WantedBy=multi-user.target

Create it with:

 1sudo tee /etc/systemd/system/ghost-manual.service >/dev/null <<'EOF'
 2[Unit]
 3Description=Ghost manual service
 4After=network.target mysql.service
 5Wants=network.target
 6
 7[Service]
 8Type=simple
 9User=<your-linux-user>
10Group=<your-linux-user>
11WorkingDirectory=/var/www/<your-domain>
12Environment=NODE_ENV=production
13ExecStart=/usr/bin/node /var/www/<your-domain>/current/index.js
14Restart=always
15RestartSec=5
16
17[Install]
18WantedBy=multi-user.target
19EOF

Enable it:

1sudo systemctl daemon-reload
2sudo systemctl enable --now ghost-manual

Check it:

1sudo systemctl status ghost-manual --no-pager -l
2sudo 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:

1sudo find /var/www/<your-domain> -type f -name esbuild -exec chmod +x {} \;

Or target the common path directly:

1sudo chmod +x /var/www/<your-domain>/versions/*/node_modules/.pnpm/@esbuild*/node_modules/@esbuild/*/bin/esbuild

Restart the service:

1sudo systemctl restart ghost-manual
2sudo systemctl status ghost-manual --no-pager -l
3sudo 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:

Admin URL:

1https://<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:

1location / {
2    proxy_pass http://127.0.0.1:2589;
3    proxy_set_header Host $http_host;
4    proxy_set_header X-Real-IP $remote_addr;
5    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
6    proxy_set_header X-Forwarded-Proto https;
7}

Test and reload:

1sudo nginx -t
2sudo systemctl reload nginx

What did not work reliably in this Debian 12 case #

These paths either failed or were unstable in this deployment:


What actually worked #

This is the short version:

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:

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.

last updated: