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