Self-Hosted Blog Setup: A Complete Guide
What Is a Self-Hosted Blog?
A self-hosted blog runs on infrastructure you control instead of a managed platform like WordPress.com, Medium, or Substack. You own the content, the database, and the domain. No platform can delete your posts, change your monetization terms, or shut down under you. You pick the software, customize it however you want, and pay only for the server.
This guide walks you through the full process: choosing a blogging platform, deploying it with Docker, putting it behind a reverse proxy with SSL, and backing it up.
Prerequisites
- A Linux server (Ubuntu 22.04+ or Debian 12+ recommended) — see Getting Started with Self-Hosting
- Docker and Docker Compose installed — see Docker Compose Basics
- A domain name pointed at your server’s IP address (A record)
- 1-2 GB of RAM (varies by platform)
- Basic familiarity with the Linux command line — see Linux Basics for Self-Hosting
- A reverse proxy for SSL termination — see Reverse Proxy Setup
Choosing a Platform
Three platforms cover the vast majority of self-hosted blog use cases: Ghost, WordPress, and Hugo. Each makes fundamentally different trade-offs.
| Feature | Ghost | WordPress | Hugo |
|---|---|---|---|
| Type | Dynamic CMS | Dynamic CMS | Static site generator |
| Language | Node.js | PHP | Go |
| Database | MySQL | MySQL / MariaDB | None (flat files) |
| Editor | Built-in Markdown + rich editor | Gutenberg block editor + plugins | Any text editor (Markdown) |
| Themes | Handlebars templates | 10,000+ themes | 300+ themes |
| Plugins | None (features are built-in) | 60,000+ plugins | Hugo modules |
| Memberships & newsletters | Built-in | Requires plugins | Not supported |
| RAM usage (idle) | ~250 MB | ~150 MB | 0 MB (static HTML) |
| Maintenance | Low | Medium-high (plugin updates, security patches) | Very low |
| SEO tools | Built-in | Via plugins (Yoast, Rank Math) | Manual or build-time |
| Self-hosting difficulty | Easy | Easy | Moderate (build pipeline needed) |
| Best for | Writers, newsletters, memberships | Complex sites needing plugins | Developers, high-performance blogs |
| License | MIT | GPL-2.0 | Apache-2.0 |
The recommendation: Ghost for most people. Ghost gives you a polished writing experience, built-in membership and newsletter features, good SEO defaults, and low maintenance overhead. You get 80% of what WordPress offers without the plugin management burden. If you need a specific WordPress plugin ecosystem feature (WooCommerce, advanced forms, LMS), use WordPress. If you want maximum performance and are comfortable with a build step, use Hugo.
For a deeper comparison, see Ghost vs WordPress.
Option 1: Deploy Ghost with Docker (Recommended)
Ghost is a Node.js application backed by MySQL. The Docker setup is straightforward.
Create a project directory:
mkdir -p ~/ghost && cd ~/ghost
Create a docker-compose.yml file:
services:
ghost:
image: ghost:5.118.0
container_name: ghost
restart: unless-stopped
environment:
database__client: mysql
database__connection__host: ghost-db
database__connection__user: ghost
database__connection__password: ${GHOST_DB_PASSWORD}
database__connection__database: ghost
# REQUIRED: Set this to your actual blog URL (with https://)
url: https://blog.example.com
NODE_ENV: production
# Uncomment and configure for newsletter/member signup emails:
# mail__transport: SMTP
# mail__options__host: smtp.mailgun.org
# mail__options__port: 587
# mail__options__auth__user: [email protected]
# mail__options__auth__pass: ${SMTP_PASSWORD}
# mail__from: [email protected]
volumes:
- ghost-content:/var/lib/ghost/content
ports:
- "2368:2368"
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: ${GHOST_DB_PASSWORD}
volumes:
- ghost-db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
volumes:
ghost-content: # Themes, images, uploaded files
ghost-db-data: # MySQL database files
Create a .env file alongside it:
# Ghost database credentials — CHANGE THESE
GHOST_DB_PASSWORD=replace-with-a-strong-password
MYSQL_ROOT_PASSWORD=replace-with-a-different-strong-password
# Uncomment if configuring SMTP
# SMTP_PASSWORD=your-smtp-password
Start the stack:
docker compose up -d
Ghost is now running on port 2368. Visit http://your-server-ip:2368/ghost/ to create your admin account. Do this immediately — the first person to visit this URL becomes the site owner.
For the full Ghost guide including theme management, member configuration, and advanced settings, see How to Self-Host Ghost.
Option 2: Deploy WordPress with Docker
WordPress uses PHP and needs either MySQL or MariaDB. MariaDB is the better choice for self-hosting — it is a drop-in MySQL replacement with better performance and truly open-source governance.
Create a project directory:
mkdir -p ~/wordpress && cd ~/wordpress
Create a docker-compose.yml file:
services:
wordpress:
image: wordpress:6.7.2-php8.3-apache
container_name: wordpress
restart: unless-stopped
environment:
WORDPRESS_DB_HOST: wordpress-db
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD}
WORDPRESS_DB_NAME: wordpress
WORDPRESS_TABLE_PREFIX: wp_
volumes:
- wordpress-data:/var/www/html
ports:
- "8080:80"
depends_on:
wordpress-db:
condition: service_healthy
networks:
- wp-net
wordpress-db:
image: mariadb:11.7
container_name: wordpress-db
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
MARIADB_DATABASE: wordpress
MARIADB_USER: wordpress
MARIADB_PASSWORD: ${WP_DB_PASSWORD}
volumes:
- wordpress-db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
networks:
- wp-net
volumes:
wordpress-data: # WordPress core files, themes, plugins, uploads
wordpress-db-data: # MariaDB database files
networks:
wp-net:
driver: bridge
Create a .env file:
# WordPress database credentials — CHANGE THESE
WP_DB_PASSWORD=replace-with-a-strong-password
MARIADB_ROOT_PASSWORD=replace-with-a-different-strong-password
Start the stack:
docker compose up -d
Visit http://your-server-ip:8080 to run the WordPress installation wizard. Choose a username other than “admin” — automated attacks target that username constantly.
For the full WordPress guide including performance tuning and plugin recommendations, see How to Self-Host WordPress.
Option 3: Deploy Hugo (Static Site)
Hugo generates static HTML files from Markdown content. There is no server-side runtime — you build the site locally or in CI, then serve the output with any web server. This means zero attack surface and near-instant page loads.
The trade-off: you need a build pipeline. Hugo does not have a web-based editor or admin panel. You write Markdown files, run hugo build, and deploy the output.
Install Hugo
On Ubuntu/Debian:
# Hugo extended edition (required for most themes)
wget https://github.com/gohugoio/hugo/releases/download/v0.145.0/hugo_extended_0.145.0_linux-amd64.deb
sudo dpkg -i hugo_extended_0.145.0_linux-amd64.deb
hugo version
Create a Site
hugo new site myblog
cd myblog
git init
Add a Theme
# Example: PaperMod theme (popular, fast, SEO-friendly)
git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
Add to hugo.toml:
baseURL = "https://blog.example.com/"
languageCode = "en-us"
title = "My Blog"
theme = "PaperMod"
[params]
defaultTheme = "auto"
ShowReadingTime = true
ShowShareButtons = true
ShowPostNavLinks = true
ShowBreadCrumbs = true
Write and Build
# Create a post
hugo new content posts/my-first-post.md
# Build the site (output goes to public/)
hugo --minify
# Serve locally for preview
hugo server -D
Serve with Docker
Once built, serve the public/ directory with Nginx or Caddy. Here is a Docker Compose setup using Nginx:
services:
hugo-blog:
image: nginx:1.27-alpine
container_name: hugo-blog
restart: unless-stopped
volumes:
- ./public:/usr/share/nginx/html:ro
ports:
- "8090:80"
Rebuild and redeploy whenever you publish new content:
hugo --minify
docker compose restart hugo-blog
For the full Hugo guide including CI/CD deployment pipelines, see How to Self-Host Hugo.
Domain and DNS Setup
Regardless of which platform you choose, your blog needs a domain name pointing to your server.
- Register a domain (or use an existing one) with any registrar — Cloudflare, Namecheap, Porkbun all work.
- Create an A record pointing to your server’s public IP address:
| Type | Name | Value | TTL |
|---|---|---|---|
| A | blog | 203.0.113.50 (your server IP) | Auto |
- Wait for DNS propagation (usually under 5 minutes with Cloudflare, up to 48 hours with some registrars).
- Verify by pinging the domain:
ping blog.example.com
If you are using Cloudflare as your DNS provider, you can also set up a Cloudflare Tunnel instead of exposing your server’s IP directly.
Reverse Proxy and SSL
Running your blog directly on port 2368 or 8080 without encryption is not acceptable for production. You need a reverse proxy to handle SSL termination and serve your site on port 443.
The simplest option for most people is Nginx Proxy Manager — it provides a web UI for managing proxy hosts and handles Let’s Encrypt certificates automatically. For a deeper look at reverse proxy concepts, see Reverse Proxy Explained.
Nginx Proxy Manager Setup
If you do not already have a reverse proxy running, add Nginx Proxy Manager to your stack:
services:
npm:
image: jc21/nginx-proxy-manager:2.14.0
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
- "81:81" # Admin UI
volumes:
- npm-data:/data
- npm-letsencrypt:/etc/letsencrypt
volumes:
npm-data:
npm-letsencrypt:
After starting it with docker compose up -d, access the admin panel at http://your-server-ip:81. Default credentials: [email protected] / changeme.
Add a proxy host:
- Domain Names:
blog.example.com - Scheme:
http - Forward Hostname / IP:
ghost(orwordpress, depending on your platform — use the container name) - Forward Port:
2368for Ghost,80for WordPress - SSL tab: Request a new SSL certificate, enable Force SSL and HTTP/2
Your blog is now accessible at https://blog.example.com with automatic certificate renewal.
For more reverse proxy options (Traefik, Caddy), see Reverse Proxy Explained.
Backup Strategy
Your blog has two critical data sources: the application data (themes, uploads, config) and the database. Lose either one and you lose the blog.
Ghost Backup
Ghost stores content in two places:
- MySQL database — all posts, settings, members, tags
- Content volume (
ghost-content) — themes, images, uploaded files
Back up both:
# Database dump
docker exec ghost-db mysqldump -u root -p"$(cat .env | grep MYSQL_ROOT_PASSWORD | cut -d= -f2)" ghost > ghost-db-backup.sql
# Content volume
docker run --rm -v ghost-content:/data -v $(pwd):/backup alpine tar czf /backup/ghost-content-backup.tar.gz /data
WordPress Backup
WordPress stores content in:
- MariaDB database — all posts, pages, settings, user accounts
- WordPress data volume (
wordpress-data) — themes, plugins, uploads inwp-content/
# Database dump
docker exec wordpress-db mariadb-dump -u root -p"$(cat .env | grep MARIADB_ROOT_PASSWORD | cut -d= -f2)" wordpress > wordpress-db-backup.sql
# Data volume
docker run --rm -v wordpress-data:/data -v $(pwd):/backup alpine tar czf /backup/wordpress-data-backup.tar.gz /data
Hugo Backup
Hugo is the simplest to back up. Your entire site is source files — Markdown, config, and theme. If you use Git (and you should), your repository is the backup. Push to a remote regularly:
git add -A && git commit -m "Content update" && git push origin main
Automate It
Schedule backups with cron. Run daily at minimum:
crontab -e
0 3 * * * /home/user/ghost/backup.sh >> /home/user/ghost/backup.log 2>&1
For a complete backup strategy including offsite storage and restore testing, see Backup Strategy.
Post-Setup Checklist
After your blog is live, verify these before publishing your first post:
| Check | Ghost | WordPress | Hugo |
|---|---|---|---|
| SSL working (padlock in browser) | Required | Required | Required |
| Admin panel accessible | /ghost/ | /wp-admin/ | N/A |
| Default credentials changed | First-visit setup | Set during install | N/A |
| Email sending configured | Optional (needed for members) | Optional (needed for notifications) | N/A |
| Permalink structure set | Automatic | Settings > Permalinks > “Post name” | Set in hugo.toml |
| Sitemap generated | Automatic at /sitemap.xml | Requires plugin (Yoast) | Built-in with config |
| RSS feed working | Automatic at /rss/ | Automatic at /feed/ | Automatic at /index.xml |
| Backup tested | Test restore from dump | Test restore from dump | Verify Git remote |
Common Mistakes
Using :latest Docker image tags. Pin to a specific version. When you run docker compose pull months later, :latest can jump major versions and break your site. Pinning gives you control over when you upgrade.
Forgetting to set the url in Ghost. Ghost uses the url environment variable to generate all internal links, sitemap entries, and canonical URLs. If this is wrong or still set to http://localhost:2368, your SEO and social sharing will break silently.
Skipping database backups. Volume backups alone are not enough. A MySQL/MariaDB data directory copied while the database is running can be corrupted. Always use mysqldump or mariadb-dump for a consistent snapshot.
Exposing the blog directly without a reverse proxy. Running on port 2368 or 8080 without SSL means credentials travel in plaintext. Always put a reverse proxy in front, even for a personal blog.
Choosing WordPress “because everyone uses it.” WordPress is powerful but requires ongoing maintenance — plugin updates, security patches, PHP version management. If you just want to write and publish, Ghost is significantly less work to maintain.
Next Steps
Your blog is deployed, encrypted, and backed up. From here:
- Write your first post and verify it renders correctly
- Configure email if you want newsletter or membership features (Ghost) or notification emails (WordPress)
- Set up monitoring to know when your blog goes down — see Uptime Monitoring
- Optimize performance with caching and CDN configuration
- Explore other self-hosted CMS options in Best Self-Hosted CMS & Websites
Related
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments