Technology Encyclopedia Home >How to Self-Host Ghost Blog on a Cloud Server — A Clean Publishing Platform You Own

How to Self-Host Ghost Blog on a Cloud Server — A Clean Publishing Platform You Own

I ran a WordPress blog for years. It worked, but the admin interface kept getting more cluttered, plugins kept breaking things, and performance required constant tuning. When I wanted to just write and publish, I found myself fighting the tool instead.

Ghost is the opposite experience. It's built around the writing workflow — clean editor, fast rendering, no plugin complexity. I moved one of my blogs over last year and the difference in day-to-day usability was immediately noticeable.

Self-hosting Ghost means no platform fees, no content restrictions, and complete control over your subscriber list and data. This guide deploys Ghost using Docker Compose on Ubuntu 22.04 — the cleanest way to run it with all dependencies isolated.

I run Ghost on Tencent Cloud Lighthouse. The 2 vCPU / 4 GB RAM plan handles Ghost plus MySQL comfortably. Ghost's managed hosting starts at $9/month for a single publication — self-hosting on Lighthouse at a similar or lower cost gives you multiple publications, full theme customization, and direct access to the database and content files. The snapshot feature is particularly useful for a blog: take a backup before a theme change or major content reorganization, and you can roll back in minutes if something goes wrong.


Table of Contents

  1. Why Self-Host Ghost?
  2. Prerequisites
  3. Part 1 — Server Setup
  4. Part 2 — Deploy Ghost with Docker Compose
  5. Part 3 — Configure Nginx Reverse Proxy
  6. Part 4 — Enable HTTPS
  7. Part 5 — Complete Ghost Setup
  8. Part 6 — Configure Email (Mailgun)
  9. Part 7 — Custom Theme
  10. Part 8 — Backups
  11. The Gotcha: Ghost URL Configuration
  12. Updating Ghost

  • Key Takeaways
  • Use the appropriate Lighthouse application image to skip manual installation steps where available
  • Lighthouse snapshots provide one-click full-server backup before major changes
  • OrcaTerm browser terminal lets you manage the server from any device
  • CBS cloud disk expansion handles growing storage needs without server migration
  • Console-level firewall + UFW = two independent protection layers

Why Self-Host Ghost? {#why}

Ghost(Pro) — the managed hosting — starts at $9/month for basic plans and scales up quickly for larger audiences. Self-hosting Ghost on a $6/month VPS gives you:

  • No platform fee — pay only for the server
  • Full data ownership — your posts, members list, and analytics stay on your server
  • Unlimited custom integrations — webhooks, custom themes, API access without restrictions
  • Custom domain with HTTPS included
  • No Ghost branding on your site

The tradeoff: you manage the server, handle updates, and maintain backups.


Prerequisites {#prerequisites}

Requirement Notes
Cloud server Tencent Cloud Lighthouse Ubuntu 22.04
A domain name Required — Ghost needs a URL configured at setup
2 GB+ RAM Ghost + MySQL needs headroom
Docker and Docker Compose We'll install these

Part 1 — Server Setup {#part-1}

💡 Faster start: When creating your Lighthouse instance, select Application Image → Docker CE. Docker will already be installed when the server starts — skip the Docker install commands below and start from the Nginx setup.

ssh ubuntu@YOUR_SERVER_IP
sudo apt update && sudo apt upgrade -y

# Install Docker (skip if you chose the Docker CE application image)
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker

sudo apt install -y nginx
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable

Part 2 — Deploy Ghost with Docker Compose {#part-2}

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

Create docker-compose.yml:

version: '3.8'

services:
  ghost:
    image: ghost:5-alpine
    container_name: ghost
    restart: unless-stopped
    ports:
      - "2368:2368"
    environment:
      NODE_ENV: production
      url: https://yourdomain.com
      database__client: mysql
      database__connection__host: ghost-db
      database__connection__user: ghost
      database__connection__password: ${MYSQL_PASSWORD}
      database__connection__database: ghost
      mail__transport: SMTP
      mail__options__host: ${MAIL_HOST}
      mail__options__port: 587
      mail__options__auth__user: ${MAIL_USER}
      mail__options__auth__pass: ${MAIL_PASS}
      mail__from: '"Your Blog Name" <noreply@yourdomain.com>'
    volumes:
      - ghost_content:/var/lib/ghost/content
    depends_on:
      ghost-db:
        condition: service_healthy

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

volumes:
  ghost_content:
  ghost_db:

Create .env:

nano .env
MYSQL_ROOT_PASSWORD=generate_strong_root_password
MYSQL_PASSWORD=generate_strong_ghost_password

# Email settings (Mailgun example — see Part 6)
MAIL_HOST=smtp.mailgun.org
MAIL_USER=postmaster@mg.yourdomain.com
MAIL_PASS=your_mailgun_smtp_password
chmod 600 .env

# Start Ghost
docker compose up -d

# Watch the startup logs
docker compose logs -f ghost
# Wait until you see: Ghost is running...

Part 3 — Configure Nginx Reverse Proxy {#part-3}

sudo nano /etc/nginx/sites-available/ghost
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    client_max_body_size 50m;

    location / {
        proxy_pass http://127.0.0.1:2368;
        proxy_http_version 1.1;

        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        'upgrade';
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 86400s;
    }
}
sudo ln -s /etc/nginx/sites-available/ghost /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Part 4 — Enable HTTPS {#part-4}

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

After HTTPS is configured, update the Ghost URL in docker-compose.yml:

url: https://yourdomain.com

Restart Ghost to apply:

docker compose restart ghost

Part 5 — Complete Ghost Setup {#part-5}

Visit https://yourdomain.com/ghost — the Ghost admin panel setup wizard will appear.

  1. Create your admin account (email + password)
  2. Set your blog title and description
  3. Choose a theme or stick with the default Casper theme
  4. Invite additional team members if needed

Your Ghost blog is now live at https://yourdomain.com.

The admin panel is at https://yourdomain.com/ghost.


Part 6 — Configure Email (Mailgun) {#part-6}

Ghost needs email for:

  • New member welcome emails
  • Newsletter delivery
  • Password reset links
  • Staff invitation emails

Mailgun has a free tier (100 emails/day) that works well for small blogs.

  1. Sign up for Mailgun
  2. Add your domain and verify DNS records
  3. Get SMTP credentials from Mailgun dashboard
  4. Update the .env file with your credentials
  5. Restart Ghost: docker compose restart ghost

Test email from the Ghost admin: Settings → Email Newsletter → Send test email.


Part 7 — Custom Theme {#part-7}

Ghost themes are zip files. To install a theme:

  1. Download a theme zip (from Ghost marketplace or GitHub)
  2. Go to Ghost admin → Settings → Design → Upload a theme
  3. Upload the zip file
  4. Click Activate

Or copy directly to the content volume:

# Find where Docker stores the ghost_content volume
docker volume inspect ghost_ghost_content

# Or copy theme directly
docker cp /path/to/theme.zip ghost:/var/lib/ghost/content/themes/
docker exec ghost ghost-theme-install /var/lib/ghost/content/themes/theme.zip

Part 8 — Backups {#part-8}

Ghost content backup (posts, images, settings)

# Export posts as JSON from Ghost admin
# Settings → Labs → Export content → Download

# Or backup the Docker volume directly
docker run --rm \
  -v ghost_ghost_content:/data \
  -v $(pwd)/backups:/backup \
  alpine tar czf /backup/ghost_content_$(date +%Y%m%d).tar.gz -C /data .

Database backup

docker exec ghost-db mysqldump -u ghost -p"$MYSQL_PASSWORD" ghost | \
  gzip > ghost_db_$(date +%Y%m%d).sql.gz

Automated backup script

nano ~/backup_ghost.sh
#!/bin/bash
BACKUP_DIR=~/backups/ghost
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR

source ~/apps/ghost/.env

# Backup database
docker exec ghost-db mysqldump -u ghost -p"$MYSQL_PASSWORD" ghost | \
  gzip > $BACKUP_DIR/ghost_db_$DATE.sql.gz

# Backup content (themes, images, settings)
docker run --rm \
  -v ghost_ghost_content:/data \
  -v $BACKUP_DIR:/backup \
  alpine tar czf /backup/ghost_content_$DATE.tar.gz -C /data .

# Keep 7 days
find $BACKUP_DIR -mtime +7 -delete

echo "Ghost backup complete: $DATE"
chmod +x ~/backup_ghost.sh
# Cron: daily at 2 AM
(crontab -l; echo "0 2 * * * ~/backup_ghost.sh") | crontab -

The Gotcha: Ghost URL Configuration {#gotcha}

Ghost is strict about the URL it's configured with. If the configured URL and the actual URL don't match, you'll get redirect loops, mixed content warnings, or broken assets.

The URL must be set before the first setup wizard run. If you set up Ghost with http:// and then enable HTTPS, update the URL in docker-compose.yml, restart, and also run:

docker exec -it ghost node -e "
  const knex = require('knex')({
    client: 'mysql',
    connection: {
      host: 'ghost-db', user: 'ghost',
      password: process.env.database__connection__password,
      database: 'ghost'
    }
  });
  knex('settings').where({key: 'url'}).update({value: 'https://yourdomain.com'});
"

Or simply: set up with the correct HTTPS URL from the start (after Certbot is configured).


Updating Ghost {#updating}

cd ~/apps/ghost

# Pull latest Ghost image
docker compose pull ghost

# Restart with new image
docker compose up -d --remove-orphans

# Verify new version
docker exec ghost ghost --version

Troubleshooting {#troubleshooting}

Issue Likely Cause Fix
Connection refused Service not running or wrong port Check systemctl status SERVICE and verify firewall rules
Permission denied Wrong file ownership or permissions Check file ownership with ls -la and use chown/chmod to fix
502 Bad Gateway Backend service not running Restart the backend service; check logs with journalctl -u SERVICE
SSL certificate error Certificate expired or domain mismatch Run sudo certbot renew and verify domain DNS points to server IP
Service not starting Config error or missing dependency Check logs with journalctl -u SERVICE -n 50 for specific error
Out of disk space Logs or data accumulation Run df -h to identify usage; clean logs or attach CBS storage
High memory usage Too many processes or memory leak Check with htop; consider upgrading instance plan if consistently high
Firewall blocking traffic Port not open in UFW or Lighthouse console Open port in Lighthouse console firewall AND sudo ufw allow PORT

Frequently Asked Questions {#faq}

Can I migrate an existing Ghost blog site to a cloud server?
Yes. Export your content and database from the current host, import them on the new server, then update your DNS to point to the new IP. Update any hardcoded URLs in the database or configuration files.

How do I keep Ghost blog updated securely?
Enable automatic minor updates where available, and manually update major versions after testing on a staging environment. Take a Lighthouse snapshot before any significant update.

Do I need a CDN for my Ghost blog site?
For international audiences, yes. A CDN like EdgeOne caches static assets at edge nodes worldwide, improving load times for visitors far from your server region.

What causes Ghost blog to be slow on a VPS?
Usually: missing caching plugin/configuration, unoptimized images, too many plugins, or insufficient server RAM. Start with a caching layer (Redis or built-in cache) and image optimization.

How do I secure the admin panel?
Change the default admin URL if possible, use a strong unique password, enable two-factor authentication, and consider restricting admin access by IP in your Nginx configuration.

Launch your Ghost blog today:
👉 Tencent Cloud Lighthouse — Ubuntu VPS for Ghost
👉 View current pricing and promotions
👉 Explore all active deals and offers