Back to Blog
Server and network equipment in a data center

Ghost Self-Hosted Deployment on DigitalOcean

Self-hosted Ghost publishing platform deployed on a DigitalOcean Droplet using the official Ghost Docker tooling.

Devin Brand 5 min read

Overview

Self-hosted Ghost publishing platform deployed on a DigitalOcean Droplet using the official Ghost Docker tooling. Caddy handles reverse proxy and automatic SSL via Let’s Encrypt. Brevo handles transactional email (login links, password resets, member signups).

SE Skills Demonstrated:

  • Infrastructure provisioning from scratch
  • Docker Compose orchestration
  • DNS configuration
  • SSL/TLS certificate management
  • SMTP email integration
  • Self-hosted deployment and troubleshooting

Prerequisites

  • DigitalOcean Droplet (Ubuntu, 2GB RAM recommended)
  • A domain with DNS access (onbrand.dev via Namecheap/Cloudflare)
  • Brevo account (free tier, SMTP credentials)
  • DNS A record pointing subdomain to Droplet IP before starting Ghost

Architecture

Internet → ghost.onbrand.dev (DNS A record → Droplet IP)
         → Caddy (reverse proxy, auto SSL via Let's Encrypt)
         → Ghost (Node.js app)
         → MySQL (database)
         → Brevo (transactional email via SMTP)

Step-by-Step Deployment

1. Access the Droplet

Via DigitalOcean browser console (Droplet → Console) or SSH:

ssh root@YOUR_DROPLET_IP

2. Install Dependencies

# Install Git
apt install git -y

# Install Docker
curl -fsSL https://get.docker.com | sh

3. Clone Ghost Docker Repository

git clone https://github.com/TryGhost/ghost-docker.git /opt/ghost && cd /opt/ghost

4. Copy Config Files

cp .env.example .env
cp caddy/Caddyfile.example caddy/Caddyfile

5. Generate Database Passwords

Generate two separate random passwords — one for the database root user, one for the Ghost database user:

openssl rand -hex 32
# Copy output → DATABASE_ROOT_PASSWORD

openssl rand -hex 32
# Copy output → DATABASE_PASSWORD

Important: Generate both passwords BEFORE opening the .env file so you have them ready to paste in.


6. Set Up DNS First

Before starting Ghost, add an A record at your DNS provider:

TypeHostnameValueTTL
AghostYOUR_DROPLET_IP300

This resolves to ghost.onbrand.dev → 104.131.44.43

Why first? Caddy provisions an SSL certificate from Let’s Encrypt on first boot. Let’s Encrypt verifies domain ownership via DNS. If the A record doesn’t exist yet, certificate provisioning fails. Too many failures trigger a rate limit (5 failures = 1 hour lockout).

Verify DNS propagation at whatsmydns.net before proceeding.


7. Configure .env File

nano .env

Fill in all required fields:

# Domain
DOMAIN=ghost.onbrand.dev

# Database (use the two passwords generated in Step 5)
DATABASE_ROOT_PASSWORD=your_first_random_password
DATABASE_PASSWORD=your_second_random_password

# SMTP Email (Brevo)
mail__transport=SMTP
mail__options__host=smtp-relay.brevo.com
mail__options__port=587
mail__options__secure=true
mail__options__auth__user=your_brevo_login_email
mail__options__auth__pass=your_brevo_smtp_api_key
mail__from="'Your Name' <[email protected]>"

Getting Brevo SMTP credentials:

  1. Sign up at brevo.com (free)
  2. Go to Settings → SMTP & API
  3. Copy your SMTP login and generate an API key

Save and exit nano:

  • Ctrl+XYEnter

8. Start Ghost

docker compose pull
docker compose up -d

9. Verify SSL Certificate

docker compose logs caddy --tail 20

Look for:

"msg":"certificate obtained successfully","identifier":"ghost.onbrand.dev"

10. Complete Setup

Visit ghost.onbrand.dev/ghost in your browser to create your admin account and complete Ghost setup.


Troubleshooting

DNS NXDOMAIN Error

DNS problem: NXDOMAIN looking up A for ghost.onbrand.dev

Cause: A record not yet set or not propagated.
Fix: Add A record at DNS provider, verify at whatsmydns.net, then restart:

cd /opt/ghost && docker compose restart

Let’s Encrypt Rate Limit

HTTP 429 - too many failed authorizations (5) in the last 1h

Cause: Too many failed SSL certificate attempts (caused by DNS not being set up before first boot).
Fix: Wait until the timestamp shown in the error, then restart. Caddy retries automatically.

”No configuration file provided” Error

no configuration file provided: not found

Cause: Running Docker Compose commands from wrong directory.
Fix:

cd /opt/ghost
docker compose restart

Checking Logs

# Caddy (SSL/proxy)
docker compose logs caddy --tail 20

# Ghost app
docker compose logs ghost --tail 20

# All services
docker compose logs --tail 20

Key Lessons

  1. Set up DNS before starting Ghost — Caddy provisions SSL on first boot. No DNS = failed cert attempts = rate limit.
  2. Generate both database passwords before editing .env — avoids having to leave nano mid-edit.
  3. The Docker install uses environment variables, not config.production.json — mail config uses mail__options__host double-underscore syntax, not JSON.
  4. AAAA records (IPv6) are not required — an A record (IPv4) is sufficient.
  5. Caddy retries automatically — after rate limit expires, no manual intervention needed beyond a restart.

Final State

ComponentStatus
Ghost✅ Running
MySQL✅ Running
Caddy✅ Running
SSL Certificate✅ Obtained (Let’s Encrypt)
DNS✅ ghost.onbrand.dev → 104.131.44.43
Email (Brevo)✅ Configured
Admin URLghost.onbrand.dev/ghost

Share this post

Devin Brand

Devin Brand

Data explorer, web builder, and eternally curious human. Always asking "why?" and digging for answers.