SELF-HOST, WITHOUT THE GUESSWORK

How to Self-Host Ghost CMS on a VPS (Production Setup)

Run Ghost CMS on your own VPS. Professional publishing platform without subscription fees.

How to Self-Host Ghost CMS on a VPS (Production Setup)

Ghost is a professional publishing platform. Clean editor, memberships, newsletters, and SEO built in.

Self-hosting Ghost means no per-member fees for paid subscriptions. Keep 100% of your membership revenue.

What you will have at the end

Step 1: Prepare the server

sudo apt update && sudo apt upgrade -y

Install Docker if needed:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker

Step 2: Create directory structure

mkdir -p ~/apps/ghost/content
cd ~/apps/ghost

Step 3: Create docker-compose.yml

services:
  traefik:
    image: traefik:v2.11
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.letsencrypt.acme.tlschallenge=true
      - [email protected]
      - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - letsencrypt:/letsencrypt
    restart: unless-stopped

  mysql:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ghost
      MYSQL_USER: ghost
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  ghost:
    image: ghost:5-alpine
    restart: unless-stopped
    depends_on:
      mysql:
        condition: service_healthy
    environment:
      url: https://blog.your-domain.com
      database__client: mysql
      database__connection__host: mysql
      database__connection__user: ghost
      database__connection__password: ${MYSQL_PASSWORD}
      database__connection__database: ghost
      mail__transport: SMTP
      mail__options__host: ${SMTP_HOST}
      mail__options__port: ${SMTP_PORT}
      mail__options__auth__user: ${SMTP_USER}
      mail__options__auth__pass: ${SMTP_PASSWORD}
    volumes:
      - ./content:/var/lib/ghost/content
    labels:
      - traefik.enable=true
      - traefik.http.routers.ghost.rule=Host(`blog.your-domain.com`)
      - traefik.http.routers.ghost.entrypoints=websecure
      - traefik.http.routers.ghost.tls=true
      - traefik.http.routers.ghost.tls.certresolver=letsencrypt
      - traefik.http.services.ghost.loadbalancer.server.port=2368

volumes:
  letsencrypt:
  mysql_data:

Step 4: Configure environment variables

Create .env file:

# Database passwords
MYSQL_ROOT_PASSWORD=$(openssl rand -hex 16)
MYSQL_PASSWORD=$(openssl rand -hex 16)

# SMTP settings (for newsletters and member emails)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your-smtp-user
SMTP_PASSWORD=your-smtp-password

cat > .env << EOF
MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD
MYSQL_PASSWORD=$MYSQL_PASSWORD
SMTP_HOST=$SMTP_HOST
SMTP_PORT=$SMTP_PORT
SMTP_USER=$SMTP_USER
SMTP_PASSWORD=$SMTP_PASSWORD
EOF

Configure SMTP for newsletters. Without it, member features are limited.

Step 5: Deploy

docker compose up -d

Wait for MySQL to initialize before Ghost starts.

Step 6: Complete setup

Visit https://blog.your-domain.com/ghost to access the admin panel.

Create your admin account and configure:

  1. Site title and description
  2. Staff users
  3. Navigation
  4. Theme (default Casper or upload custom)

Step 7: Configure memberships (optional)

For paid subscriptions:

  1. Go to Settings > Membership
  2. Connect Stripe for payments
  3. Configure subscription tiers
  4. Customize signup portal

Ghost handles member management, payments, and email delivery.

Step 8: Configure newsletters

  1. Go to Settings > Email newsletter
  2. Configure sender details
  3. Connect to Mailgun or use direct SMTP
  4. Test email delivery

Backup strategy

Ghost needs MySQL and content directory backups.

#!/bin/bash
# Daily backup script
BACKUP_DIR=/backups/ghost/$(date +%Y%m%d)
mkdir -p $BACKUP_DIR

# MySQL dump
docker exec ghost-mysql-1 mysqldump -u ghost -p$MYSQL_PASSWORD ghost > $BACKUP_DIR/ghost.sql

# Content directory (images, themes, settings)
tar -czf $BACKUP_DIR/content.tar.gz ~/apps/ghost/content

# Compress everything
tar -czf $BACKUP_DIR.tar.gz $BACKUP_DIR

Store backups off-server. Consider automated S3 uploads.

Troubleshooting

Cannot access admin panel

Images not uploading

Emails not sending

Site shows wrong URL

Internal links

From across the StoicSoft network

Hand-curated reads on the same topic from sister sites in the StoicSoft family.