Build Log: Ghost CMS on Linux VPS — Nginx, systemd & SQLite-Direct Publishing
Build Log Review 2026
TL;DR
Ghost CMS on a $12/mo Linux VPS with Nginx + systemd and SQLite-direct publishing is a viable alternative to Ghost Pro ($2,088/yr for 6 sites). Key wins: sub-200ms burst publishing, no rate limits, and full control over the stack — but you trade Ghost Pro's managed backups, automatic updates, and 99.99% uptime guarantee.
The Bottom Line
Setting up Ghost CMS on a Linux VPS with Nginx, systemd, and a SQLite-direct publishing pipeline is a robust alternative to Ghost Pro hosting. This build log covers the full architecture, configuration, and lessons learned from a 2-day implementation.
Architecture Overview
Stack: Ghost 6.35.0 + SQLite + Nginx + systemd on a Linux VPS (4GB RAM, 2 vCPU). Ghost runs as a systemd user service behind Nginx as a reverse proxy. The SQLite database is stored at ~/blog/content/data/ghost.db.
Key decision: SQLite over MySQL. For a single-author blog with <10K monthly visitors, SQLite offers simpler backups, no separate DB process, and adequate performance. The trade-off is no concurrent write support — but Ghost handles this with a built-in write queue.
Nginx Reverse Proxy Config
SSL termination handled via Let's Encrypt with Certbot. Key Nginx directives:
proxy_pass http://127.0.0.1:2369;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_cache_path /var/cache/nginx/ghost levels=1:2 keys_zone=ghost:10m max_size=1g;
proxy_cache_valid 200 30d; # Static assets cached for 30 days
systemd Service Unit
Ghost runs as a user service with Node.js heap tuning:
[Service]
ExecStart=/usr/bin/node --max-old-space-size=1024 current/index.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production
StandardOutput=journal
StandardError=journal
Automated Publishing Pipeline
The key innovation: bypassing the Ghost Admin API for automated publishing. Instead of waiting ~3s per API call, we do direct SQLite inserts completing in ~200ms. This enables burst publishing of multiple posts without hitting API rate limits.
The pipeline works by inserting directly into the posts, posts_tags, posts_authors, posts_meta, and post_revisions tables, then restarting Ghost to pick up the changes.
Backup & Disaster Recovery
Dual strategy: file-level rsync of the SQLite database (hourly) + Ghost JSON export via Admin API (daily). The rsync approach enables point-in-time recovery, while JSON export provides portable content files compatible with Ghost Pro migration.
What This Means for You
Here's how to apply this setup to your own deployment:
- Start with Ghost Pro if under 1K visitors/mo. The $9/mo plan is cheaper than a $12 VPS + your time to maintain it. Only self-host when you need custom publishing pipelines or multiple sites.
- Use SQLite-direct publishing only for trusted cron pipelines. Bypassing the Admin API means no content validation. One bad SQL UPDATE can corrupt the database — always backup before automated runs (
cp ghost.db ghost.db.pre_publish). - Monitor Ghost memory usage. With Node.js heap at 1GB, a 4GB RAM VPS leaves room for Nginx, the OS, and one more Node process. At 2GB RAM, switch to
--max-old-space-size=512and disable unused features.
For reference, see the Ghost configuration guide and Nginx proxy module docs for production tuning.
Lessons Learned
- SQLite vs MySQL: SQLite is fine for single-user blogs. Switch to MySQL at 100K+ monthly visits.
- systemd vs Docker: systemd is simpler for single-instance deployments. Docker adds orchestration overhead for marginal benefit.
- SQLite-direct tradeoffs: Fast publishing, but bypasses Ghost's content validation and hook system. Use only for trusted automated pipelines.
- Memory: Ghost with Node.js needs at least 1GB RAM. With multiple workers, 2GB+ recommended.