Setting Up Ghost Blog on Red Hat Enterprise Linux: My Homelab Journey Begins
Complete guide to installing Ghost CMS on Red Hat Enterprise Linux 9.7 with remote MariaDB, Apache reverse proxy, and SELinux. Includes all commands, troubleshooting, and lessons learned.
Introduction
Today marks the beginning of my homelab journey. I'm setting up a Ghost blog to document everything I learn as I build a Kubernetes cluster on a TuringPi board with 4 RK1 nodes. But first, I needed a place to write about it.
This post documents the complete process of installing Ghost CMS on Red Hat Enterprise Linux 9.7, connected to a remote MariaDB database, and served through Apache with SSL. Every command, every issue, and every solution - documented as it happened.
π This is part of the Blog Infrastructure series - documenting how I built this platform to share my homelab journey.
Other posts in this series:
- Setting Up Ghost on Red Hat (you are here)
- Configuring Ghost for Technical Blogging

Why Ghost?
I chose Ghost for several reasons:
- Modern and fast - Node.js-based, lightweight
- Self-hosted - Full control over my data
- Clean writing experience - Focus on content, not configuration
- Professional output - Publication-quality blog out of the box
My Infrastructure
Before starting, here's what I had:
- Web Server: Red Hat Enterprise Linux 9.7 (hostname: sulu.luwte.net)
- IP: 10.0.1.12 (IPv4) / 2a10:3781:4bc9:11:be24:11ff:fe63:6dd8 (IPv6)
- Existing LAMP stack with multiple sites
- Apache with SSL certificates
- Database Server: Separate server running MariaDB 10.3.39 (hostname: data.luwte.net)
- Domain: vluwte.nl (with existing SSL certificates)
- User: Created a ghost system user for the application
My setup uses a numbered Apache configuration system (010-vluwte.nl-ssl.conf) and stores sites in /opt/websites/domainname/.
Installation Process
Step 1: Install Node.js 22
Ghost 6.19.0 requires Node.js 22. I started with Node.js 20, but quickly discovered the version mismatch.
# Enable Node.js 22 module
dnf module enable nodejs:22 -y
# Update and install
dnf update
dnf install nodejs -y
# Verify installation
node --version # v22.19.0
npm --version # v10.9.3
Lesson learned: Always check Ghost's Node.js requirements before installing. I had to upgrade from Node.js 20 to 22 mid-installation.
Step 2: Create Ghost User
# Create system user with home directory
useradd -r -m -d /home/ghost ghost
# Verify
grep ghost /etc/passwd
grep ghost /etc/group
Note: I initially made a typo with duplicate -d flags. Pay attention to command syntax!
Step 3: Install Ghost-CLI
# Install Ghost-CLI globally
npm install ghost-cli@latest -g
# Verify
ghost --version # Ghost-CLI version: 1.28.4
You'll see some deprecation warnings about inflight and glob - these are normal and don't affect functionality.
Step 4: Create Directory Structure
# Create Ghost installation directory
mkdir -p /opt/websites/vluwte.nl
chown ghost:ghost /opt/websites/vluwte.nl
chmod 775 /opt/websites/vluwte.nl
# Verify permissions
ls -lad /opt/websites/vluwte.nl
# drwxrwxr-x. 2 ghost ghost 6 Feb 14 10:37 /opt/websites/vluwte.nl
Step 5: Set Up Database
On my MariaDB server (data.luwte.net):
-- Create database with correct collation for Ghost
CREATE DATABASE ghost_vluwte CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Create users for both IPv4 and IPv6 (MariaDB handles the hashing automatically)
CREATE USER 'ghost'@'10.0.1.12' IDENTIFIED BY 'your_password_here';
CREATE USER 'ghost'@'2a10:3781:4bc9:11:be24:11ff:fe63:6dd8' IDENTIFIED BY 'your_password_here';
-- Grant permissions
GRANT ALL PRIVILEGES ON ghost_vluwte.* TO 'ghost'@'10.0.1.12';
GRANT ALL PRIVILEGES ON ghost_vluwte.* TO 'ghost'@'2a10:3781:4bc9:11:be24:11ff:fe63:6dd8';
FLUSH PRIVILEGES;
-- Verify
SELECT User, Host FROM mysql.user WHERE User='ghost';
Important: Create database users for both IPv4 and IPv6 addresses. Use the same plaintext password here and in Ghost's config file β MariaDB handles the hashing internally.
Step 6: Fix Ghost User Permissions
The ghost user couldn't initially access the Ghost-CLI command:
# Problem: ghost user couldn't find the ghost command
su - ghost
ghost --version # command not found
# Solution: Fix permissions on node_modules
exit
chmod -R 755 /usr/local/lib/node_modules
# Test again
su - ghost
ghost --version # Success!
Lesson learned: When installing npm packages globally as root, ensure the ghost user can read /usr/local/lib/node_modules.
Step 7: Install Ghost
# Switch to ghost user
su - ghost
cd /opt/websites/vluwte.nl
# Install Ghost without setup (we'll configure manually)
ghost install --no-setup --no-start --db mysql
Output:
β Checking system Node.js version - found v22.19.0
β Checking current folder permissions
β Checking memory availability
β Checking free space
β Checking for latest Ghost version
β Setting up install directory
β Downloading and installing Ghost v6.19.0
β Finishing install process
Note: I initially tried ghost install local which attempted to use SQLite. Since I'm using MySQL, I had to clean up and reinstall with the --db mysql flag.
Step 8: Configure Ghost for Production
Create the production configuration file:
# As ghost user
vi /opt/websites/vluwte.nl/config.production.json
Configuration:
{
"url": "https://vluwte.nl",
"server": {
"port": 2368,
"host": "127.0.0.1"
},
"database": {
"client": "mysql",
"connection": {
"host": "data.luwte.net",
"port": 3306,
"user": "ghost",
"password": "your_plaintext_password_here",
"database": "ghost_vluwte"
}
},
"mail": {
"transport": "Direct"
},
"logging": {
"transports": ["file", "stdout"]
},
"process": "systemd",
"paths": {
"contentPath": "/opt/websites/vluwte.nl/content"
}
}
β οΈ CRITICAL: Use your plaintext password in the config file β the same one you used when creating the database user. Check permissions are set correctly for the configuration file after editing.
Step 9: Test Ghost Manually
Before setting up systemd, test that Ghost can connect to the database:
# As ghost user
cd /opt/websites/vluwte.nl
NODE_ENV=production node current/index.js
Output:
[2026-02-14 11:35:47] INFO Ghost is running in production...
[2026-02-14 11:35:47] INFO Your site is now available on https://vluwte.nl/
[2026-02-14 11:35:47] INFO Ctrl+C to shut down
[2026-02-14 11:35:47] INFO Ghost server started in 1.687s
[2026-02-14 11:35:48] WARN Database state requires initialisation.
[2026-02-14 11:35:49] INFO Creating table: newsletters
[... 70+ tables created ...]
[2026-02-14 11:36:01] INFO Database is in a ready state.
[2026-02-14 11:36:01] INFO Ghost database ready in 15.569s
[2026-02-14 11:36:04] INFO Ghost booted in 19.186s
Success! Ghost connected to the database and initialized all tables. Press Ctrl+C to stop.
Step 10: Create Systemd Service
# Exit to root user
exit
# Create service file
vi /etc/systemd/system/ghost-vluwte.service
Service configuration:
[Unit]
Description=Ghost Blog - vluwte.nl
Documentation=https://ghost.org/docs/
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/websites/vluwte.nl
User=ghost
Environment="NODE_ENV=production"
ExecStart=/usr/bin/node /opt/websites/vluwte.nl/current/index.js
Restart=always
SyslogIdentifier=ghost-vluwte
[Install]
WantedBy=multi-user.target
Enable and start the service:
# Reload systemd
systemctl daemon-reload
# Enable on boot
systemctl enable ghost-vluwte
# Start service
systemctl start ghost-vluwte
# Check status (press 'q' to exit)
systemctl status ghost-vluwte
Output:
β ghost-vluwte.service - Ghost Blog - vluwte.nl
Loaded: loaded (/etc/systemd/system/ghost-vluwte.service; enabled)
Active: active (running) since Sat 2026-02-14 11:38:24 CET
Main PID: 1181510 (node)
Tasks: 11
Memory: 135.6M
Note that I am bypassing ghost setup because I'm using Apache instead of the CLI's default Nginx/Systemd automation.
Step 11: Configure SELinux
Red Hat requires SELinux configuration for Apache to proxy to Ghost:
# Allow Apache to make network connections
setsebool -P httpd_can_network_connect 1
# Set proper context for Ghost directory
semanage fcontext -a -t httpd_sys_rw_content_t "/opt/websites/vluwte.nl(/.*)?"
restorecon -Rv /opt/websites/vluwte.nl
# Allow Apache to read SSL certificates
setsebool -P httpd_read_user_content 1
The restorecon command will produce extensive output as it relabels all Ghost files - this is normal.
Note that since the database is on a separate server (data.luwte.net), Apache isn't the only thing needing network accessβGhost (Node.js) does too. However, since Ghost is running as a system service, it usually isn't restricted to make a connection over the network.
Step 12: Verify Required Apache Modules
# Check all required modules in one command
httpd -M | egrep "proxy|ssl|headers|rewrite"
Output:
rewrite_module (shared)
headers_module (shared)
proxy_module (shared)
proxy_http_module (shared)
ssl_module (shared)
[... other proxy modules ...]
Required modules:
rewrite_module- for possible future themesproxy_module- Core proxy functionalityproxy_http_module- HTTP proxy supportssl_module- SSL/TLS supportheaders_module- Request header modification
All modules were already enabled on my system.
Step 13: Create Apache Virtual Host
Create logs directory first:
# Create logs directory with proper permissions
mkdir -p /opt/websites/vluwte.nl/logs
chown ghost:ghost /opt/websites/vluwte.nl/logs
chmod 755 /opt/websites/vluwte.nl/logs
Create the virtual host configuration:
# Using my numbered naming convention
vi /etc/httpd/conf.d/010-vluwte.nl-ssl.conf
Configuration:
# HTTP - Redirect to HTTPS
<VirtualHost *:80>
ServerName vluwte.nl
ServerAlias www.vluwte.nl
# Redirect all HTTP to HTTPS
Redirect permanent / https://vluwte.nl/
</VirtualHost>
# HTTPS - Ghost Proxy
<VirtualHost *:443>
ServerName vluwte.nl
ServerAlias www.vluwte.nl
# SSL Configuration (using my existing certificates)
SSLEngine on
SSLCertificateFile /etc/ssl/vluwte.nl/vluwte.nl.cer
SSLCertificateKeyFile /etc/ssl/vluwte.nl/vluwte.nl.key
SSLCertificateChainFile /etc/ssl/vluwte.nl/fullchain.cer
# Modern SSL configuration
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite HIGH:!aNULL:!MD5
SSLHonorCipherOrder on
# Logging (using my directory structure)
ErrorLog /opt/websites/vluwte.nl/logs/error.log
CustomLog /opt/websites/vluwte.nl/logs/access.log combined
# 1. Enable Rewrite Engine
RewriteEngine On
# 2. Handle WebSocket Upgrades first
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) ws://127.0.0.1:2368/$1 [P,L]
# 3. Proxy to Ghost
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:2368/
ProxyPassReverse / http://127.0.0.1:2368/
# Proxy timeout settings
ProxyTimeout 300
# Headers for proxy
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
</VirtualHost>
Note: Adjust paths for your SSL certificates and logs to match your infrastructure.
Step 14: Test and Restart Apache
# Test configuration syntax
httpd -t
# Syntax OK
# Restart Apache (I prefer apachectl over systemctl)
apachectl restart
# Check status (press 'q' to exit)
apachectl status
Output:
β httpd.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled)
Active: active (running) since Sat 2026-02-14 19:25:40 CET
Status: "Started, listening on: port 8443, port 443, ..."
Step 15: Verify Everything Works
Test Ghost is accessible through Apache:
# Test HTTPS response
curl -I https://vluwte.nl
Output:
HTTP/1.1 200 OK
Date: Sat, 14 Feb 2026 18:26:20 GMT
Server: Apache/2.4.62 (Red Hat Enterprise Linux) OpenSSL/3.5.1
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Check Apache logs to confirm traffic:
tail /opt/websites/vluwte.nl/logs/access.log
Output:
2a10:3781:4bc9:11:be24:11ff:fe63:6dd8 - - [14/Feb/2026:19:26:20 +0100] "GET /favicon.ico HTTP/1.1" 200 15406
2a10:3781:4bc9:11:be24:11ff:fe63:6dd8 - - [14/Feb/2026:19:26:18 +0100] "HEAD / HTTP/1.1" 200
Tip: Check timestamps to confirm these are new log entries from your test.
Step 16: Initial Ghost Setup
Visit https://vluwte.nl/ghost in your browser for first-time setup:
- Site Title: vLuwte's Homelab Journey
- Full Name: Igor
- Email Address: igor@vluwte.nl
- Password: [Secure password]
This creates the Owner account with full admin privileges.
Success! π
Ghost is now running at https://vluwte.nl with:
- β Node.js 22.19.0
- β Ghost v6.19.0
- β MariaDB backend (data.luwte.net)
- β Apache reverse proxy with SSL
- β Systemd service (auto-starts on boot)
- β Proper SELinux configuration
- β Logging to custom directory
Useful Management Commands
# Ghost service management
systemctl status ghost-vluwte
systemctl restart ghost-vluwte
journalctl -u ghost-vluwte -f
# Apache management
apachectl restart
apachectl status
tail -f /opt/websites/vluwte.nl/logs/access.log
tail -f /opt/websites/vluwte.nl/logs/error.log
# Ghost configuration
vi /opt/websites/vluwte.nl/config.production.json
Issues Encountered & Solutions
Issue 1: Node.js Version Mismatch
Problem: Ghost 6.19.0 requires Node.js 22, but I installed Node.js 20 first.
Solution:
dnf module reset nodejs -y
dnf module enable nodejs:22 -y
dnf install nodejs -y
Issue 2: Ghost User Can't Access ghost-cli
Problem: The ghost user couldn't find the globally installed ghost command.
Solution: Fix permissions on /usr/local/lib/node_modules:
chmod -R 755 /usr/local/lib/node_modules
Issue 3: Wrong Database User Creation
Problem: Initially created database user with placeholder IP instead of actual server IP.
Solution: Drop the incorrect user and recreate with correct IPv4 and IPv6 addresses:
-- Remove the old, incorrect user
DROP USER 'ghost'@'placeholder_ip';
-- Recreate with correct server IPs
CREATE USER 'ghost'@'10.0.1.12' IDENTIFIED BY 'actual_password';
CREATE USER 'ghost'@'2a10:3781:4bc9:11:be24:11ff:fe63:6dd8' IDENTIFIED BY 'actual_password';
-- Re-apply permissions
GRANT ALL PRIVILEGES ON ghost_vluwte.* TO 'ghost'@'10.0.1.12';
GRANT ALL PRIVILEGES ON ghost_vluwte.* TO 'ghost'@'2a10:3781:4bc9:11:be24:11ff:fe63:6dd8';
FLUSH PRIVILEGES;
Issue 4: Partial Ghost Installation
Problem: First installation attempt with ghost install local created SQLite-based setup instead of MySQL.
Solution: Clean up and reinstall with proper flags:
rm -rf config.development.json content current versions .ghost-cli
ghost install --no-setup --no-start --db mysql
Lessons Learned
- Check version requirements first - Would have saved time if I'd verified Node.js requirements before starting
- Read command output carefully - The initial useradd typo was preventable
- Database users need both IPv4 and IPv6 - Don't forget dual-stack networking
- Same password everywhere - Use the same plaintext password when creating the database user and in Ghost's config file; MariaDB handles hashing internally
- Test before automating - Running Ghost manually first revealed the database connection worked
- SELinux isn't optional on RHEL - Proper configuration is required for Apache proxying
- Document as you go - Taking notes during installation made this post much easier to write
What's Next?
Now that the blog platform is running, my next steps are:
- Configure Ghost settings - Syntax highlighting, timezone, navigation
- Write about the TuringPi - Document my journey building a 4-node RK1 cluster
- Learn Talos Linux - Set up Kubernetes on the cluster
- Self-host services - Git repository (Gitea), monitoring (Prometheus/Grafana), notes (Trilium)
- Learn Ansible - Automate cluster management
- Set up GUI management - Eventually manage everything from a web interface
Follow along as I document every step of building a production-ready homelab!
Conclusion
Setting up Ghost on Red Hat with a remote database and Apache proxy was more involved than a simple Docker deployment, but now I have:
- Full control over the infrastructure
- Integration with my existing web server setup
- Professional blogging platform ready for monetization
- Foundation for documenting my homelab journey
Total installation time: About 2 hours (including troubleshooting and documentation).
Was it worth the extra complexity? Absolutely. This setup will scale as I build out my cluster, and I've learned valuable lessons about Node.js applications, systemd services, and Apache reverse proxies.
If you're following along with your own Ghost installation, I hope this detailed walkthrough helps you avoid some of the issues I encountered!
βNext: Configuring Ghost for Technical Blogging: SEO, Syntax Highlighting, and Comments
Ready to make your Ghost blog production-ready? Check out the next post where I configure syntax highlighting, set up Google Search Console, and add a comment system.
Questions or suggestions? Leave a comment below or reach out at igor@vluwte.nl.