Self-Hosting Ghost: Professional Blogging Platform with Docker

WordPress runs 40% of the web. Ghost wants to be the reason it doesn’t run 50%.

Ghost is an open-source publishing platform built on Node.js that does three things extremely well: writing, newsletters, and memberships. The editor is fast, the output is clean, and the built-in newsletter system means you don’t need a separate Mailchimp or Buttondown subscription for basic email distribution.

The catch: Ghost(Pro) hosting starts at $9/month and scales quickly. Self-hosting Ghost gives you the same platform for the cost of your server — no seat limits, no subscriber caps, and full control over your data.

Ghost vs Other Platforms

FeatureGhostWordPressHugo/StaticWriteFreely
EditorBlock-based, MarkdownGutenberg/ClassicMarkdown filesMarkdown
Built-in newsletters✅ Native❌ Plugin needed❌ No❌ No
Memberships/payments✅ Stripe integration❌ Plugin needed❌ No❌ No
SpeedFast (Node.js)VariableBlazing (static)Fast
Themes~100 official + customThousandsUnlimitedLimited
Resource usage~300MB RAM~200MB+ RAM~0 (build time only)~50MB RAM
Self-host complexityMediumMediumEasyEasy
SEO featuresBuilt-inPlugin dependentManualBasic
APIFull Content + Admin APIREST APIBuild-time onlyLimited

Ghost sits between the simplicity of static sites and the feature bloat of WordPress. If you want a clean writing experience with newsletters and memberships built in, it’s hard to beat.

Prerequisites

  • A Linux server with Docker and Docker Compose installed (Ubuntu setup guide)
  • A domain name pointed at your server’s public IP
  • A reverse proxy (Caddy or Nginx) for HTTPS
  • At least 1GB RAM (Ghost recommends 1GB minimum)
  • An SMTP provider for transactional emails (Mailgun, Amazon SES, or any SMTP relay)

Docker Compose Setup

Ghost needs a database backend. The official image supports MySQL 8, and we’ll use that:

# docker-compose.yml
services:
  ghost:
    image: ghost:5
    container_name: ghost
    restart: unless-stopped
    ports:
      - "2368:2368"
    environment:
      url: https://blog.yourdomain.com
      database__client: mysql
      database__connection__host: ghost-db
      database__connection__user: ghost
      database__connection__password: ${GHOST_DB_PASSWORD}
      database__connection__database: ghost
      mail__transport: SMTP
      mail__options__host: ${MAIL_HOST}
      mail__options__port: ${MAIL_PORT}
      mail__options__auth__user: ${MAIL_USER}
      mail__options__auth__pass: ${MAIL_PASSWORD}
      mail__from: "Blog <[email protected]>"
    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_USER: ghost
      MYSQL_PASSWORD: ${GHOST_DB_PASSWORD}
      MYSQL_DATABASE: ghost
    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 your environment file:

# .env
GHOST_DB_PASSWORD=your-strong-database-password
MYSQL_ROOT_PASSWORD=your-strong-root-password
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_USER=[email protected]
MAIL_PASSWORD=your-mailgun-api-key

Start it up:

docker compose up -d

Ghost takes 30–60 seconds to initialize on first boot. Watch the logs:

docker compose logs -f ghost

Once you see Ghost booted, navigate to https://blog.yourdomain.com/ghost to start the setup wizard.

Initial Configuration

The setup wizard at /ghost walks you through:

  1. Create your admin account — This becomes the Owner role. Use a real email; password recovery depends on it.
  2. Set your site title and description — These appear in SEO metadata and RSS feeds.
  3. Invite staff (optional) — Add editors, authors, or contributors.

After setup, the admin panel lives at /ghost. Bookmark it.

Key Settings to Configure First

Navigate to Settings and hit these immediately:

  • General → Publication language — Set your locale (affects date formatting and SEO lang attribute)
  • General → Meta data — Set your default meta title and description for search engines
  • General → Social accounts — Add Twitter/X and Facebook URLs for social cards
  • Email newsletter → Sending domain — Configure your verified sending domain
  • Membership → Access — Choose your default access level (public, members-only, or paid)

The Ghost Editor

Ghost’s editor is its killer feature. It’s a block-based, Markdown-compatible editor that gets out of your way:

  • Markdown shortcuts work inline — type ## for headings, > for quotes, ` for code
  • Drag-and-drop images with automatic optimization
  • Embeds for YouTube, Twitter, CodePen, and dozens of other services via / commands
  • Code blocks with syntax highlighting (uses Prism.js)
  • Dynamic cards — toggles, callouts, buttons, bookmark cards, email CTAs

Type / in the editor to see all available card types. The bookmark card is particularly useful — paste a URL and Ghost generates a rich preview card automatically.

Memberships and Newsletters

Ghost’s built-in membership system connects directly to Stripe for paid subscriptions. Even if you don’t plan to charge, the free tier membership is useful for building an email list.

Setting Up Free Memberships

  1. Go to Settings → Membership
  2. Enable “Anyone can sign up”
  3. Choose default post access (public vs. members-only)
  4. Customize the signup page and portal appearance

Setting Up Paid Tiers

  1. Connect Stripe — Settings → Membership → Connect to Stripe (uses Stripe Connect, not raw API keys)
  2. Create pricing — Set monthly and yearly prices for your premium tier
  3. Gate content — When writing a post, use the access dropdown to set it as “Paid members only”

Ghost handles the entire payment flow: checkout, receipts, cancellation, and billing portal. Your readers manage their own subscriptions.

Newsletters

Every Ghost site gets a built-in newsletter. When you publish a post:

  1. Toggle “Email newsletter” in the publish flow
  2. Choose which segment to send to (free, paid, specific tier, specific label)
  3. Ghost sends a formatted HTML email matching your newsletter design

For reliable delivery at scale, you need a transactional email provider. Ghost recommends Mailgun (cheapest for most self-hosters at ~$0.80/1,000 emails), but any SMTP relay works.

Themes

Ghost ships with Casper — a clean, modern theme. To install a different theme:

  1. Download a .zip theme file from the Ghost Marketplace or GitHub
  2. Go to Settings → Design → Change theme → Upload theme
  3. Activate it
  • Casper — Default, magazine-style layout
  • Edition — Newsletter-focused design
  • Solo — Single-author minimalist blog
  • Starter — Bare-bones template for custom development

Custom Theme Development

Ghost themes use Handlebars templating. The basic structure:

my-thpdipptaaeaenoaausmcfdsggtsekaete.he/aux..hotcjgl.hhbrsssethbbs./s..bsshjhsbsbsosn######BHSSTAaoitausmnagteegthpliaolaecrragcyepphpooaiausgvgtteeesss

Test themes locally with ghost install local or use the Ghost VS Code extension for live preview.

Reverse Proxy Configuration

Caddy

b}log.ryeovuerrdsoem_apirno.xcyomgh{ost:2368

That’s it. Caddy handles HTTPS automatically.

Nginx

server {
    listen 443 ssl http2;
    server_name blog.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/blog.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/blog.yourdomain.com/privkey.pem;

    client_max_body_size 50m;

    location / {
        proxy_pass http://localhost:2368;
        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;
    }
}

Important: Set client_max_body_size high enough for image uploads. Ghost’s default upload limit is 50MB.

Ghost Content API

Ghost provides two APIs for building custom frontends or integrations:

Content API (read-only, public)

Create an integration at Settings → Integrations → Add custom integration to get a Content API key:

curl "https://blog.yourdomain.com/ghost/api/content/posts/?key=YOUR_CONTENT_API_KEY"

Use this for:

  • Headless CMS setups (Next.js, Gatsby, Astro frontends)
  • Displaying recent posts on another site
  • Building custom search

Admin API (full access)

The Admin API uses JWT authentication and can create/edit/delete content. Use it for:

  • Automated publishing pipelines
  • Migrating content from other platforms
  • Bulk editing posts or tags

Backup and Restore

Database Backup

docker exec ghost-db mysqldump -u ghost -p ghost > ghost-backup-$(date +%F).sql

Content Backup

Ghost stores images, themes, and files in the content volume:

docker run --rm -v ghost_content:/data -v $(pwd):/backup alpine \
  tar czf /backup/ghost-content-$(date +%F).tar.gz /data

Full Restore

# Restore database
cat ghost-backup-2026-03-20.sql | docker exec -i ghost-db mysql -u ghost -p ghost

# Restore content volume
docker run --rm -v ghost_content:/data -v $(pwd):/backup alpine \
  tar xzf /backup/ghost-content-2026-03-20.tar.gz -C /

Built-in Export

Ghost also has a JSON export at Settings → Labs → Export content. This exports posts, pages, tags, and settings — but not images. Use it as a secondary backup alongside volume snapshots.

Updating Ghost

docker compose pull ghost
docker compose up -d ghost

Ghost handles database migrations automatically on startup. Always back up before updating — major version upgrades (e.g., 5.x → 6.x) occasionally have breaking changes.

Check the Ghost changelog before upgrading to review breaking changes.

Troubleshooting

Ghost Shows “503 Service Unavailable”

Ghost is still booting or crashed. Check logs:

docker compose logs ghost

Common causes:

  • Database not ready yet (add the depends_on healthcheck shown above)
  • Wrong url environment variable (must match your actual domain with protocol)
  • MySQL connection refused (check credentials match between services)

Images Not Loading

Usually a url mismatch. Ghost generates image URLs based on the url environment variable. If you access the site via a different URL than configured, images break.

# Fix: ensure url matches your actual access URL
url: https://blog.yourdomain.com  # NOT http://, NOT localhost

Email Not Sending

Test your SMTP config:

docker exec -it ghost ghost config mail

Common issues:

  • Port 25 blocked by cloud provider (use 587 or 465 instead)
  • Missing authentication credentials
  • Mailgun sandbox mode (verify your sending domain)
  • SPF/DKIM records not configured for your domain

High Memory Usage

Ghost’s Node.js process can grow. Limit it in Docker:

services:
  ghost:
    # ... existing config ...
    deploy:
      resources:
        limits:
          memory: 512M

If Ghost crashes from OOM, increase to 768M or 1G.

“URL must be provided” Error on Boot

The url environment variable is required. It must be the full URL including protocol:

# Correct
url: https://blog.yourdomain.com

# Wrong
url: blog.yourdomain.com
url: http://localhost:2368

SEO and Performance Tips

Ghost has solid built-in SEO — sitemaps, structured data, canonical URLs, and meta tags are automatic. A few additional optimizations:

  • Enable built-in caching — Ghost caches aggressively by default. Don’t add extra caching layers unless you’re handling massive traffic.
  • Optimize images — Ghost auto-resizes images, but upload WebP or AVIF when possible for smaller file sizes.
  • Use the <meta> injection — Settings → Code Injection lets you add analytics, verification tags, or custom <head> content without modifying themes.
  • Internal linking — Ghost’s bookmark cards create rich internal links that improve both UX and SEO.
  • Set canonical URLs — If cross-posting to Medium or Dev.to, set the canonical URL in Ghost to avoid duplicate content penalties.

Integrations Worth Setting Up

  • Umami or Plausible for privacy-friendly analytics
  • Zapier/n8n for automation (new post → tweet, new member → CRM)
  • Comments — Ghost has native comments for members, or integrate Disqus/Cactus Comments
  • Search — Ghost has built-in search (Sodo Search). For advanced needs, use Algolia via the Content API

Wrapping Up

Ghost gives you a professional publishing platform with newsletters and memberships built in — no plugins, no bolt-ons. Self-hosting removes the subscriber limits and monthly fees of Ghost(Pro), and you keep full ownership of your content and member data.

The main trade-off is maintenance: you’re responsible for updates, backups, and email deliverability. But if you’re already running Docker services behind a reverse proxy, adding Ghost is straightforward.

For backup automation, check out our Kopia guide. For securing Ghost behind SSO, see Authentik or Authelia.