Ghost Self-Hosted Deployment on DigitalOcean
Self-hosted Ghost publishing platform deployed on a DigitalOcean Droplet using the official Ghost Docker tooling.
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:
| Type | Hostname | Value | TTL |
|---|---|---|---|
| A | ghost | YOUR_DROPLET_IP | 300 |
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:
- Sign up at brevo.com (free)
- Go to Settings → SMTP & API
- Copy your SMTP login and generate an API key
Save and exit nano:
Ctrl+X→Y→Enter
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
- Set up DNS before starting Ghost — Caddy provisions SSL on first boot. No DNS = failed cert attempts = rate limit.
- Generate both database passwords before editing .env — avoids having to leave nano mid-edit.
- The Docker install uses environment variables, not
config.production.json— mail config usesmail__options__hostdouble-underscore syntax, not JSON. - AAAA records (IPv6) are not required — an A record (IPv4) is sufficient.
- Caddy retries automatically — after rate limit expires, no manual intervention needed beyond a restart.
Final State
| Component | Status |
|---|---|
| Ghost | ✅ Running |
| MySQL | ✅ Running |
| Caddy | ✅ Running |
| SSL Certificate | ✅ Obtained (Let’s Encrypt) |
| DNS | ✅ ghost.onbrand.dev → 104.131.44.43 |
| Email (Brevo) | ✅ Configured |
| Admin URL | ghost.onbrand.dev/ghost |
Devin Brand
Data explorer, web builder, and eternally curious human. Always asking "why?" and digging for answers.