This project provides automated Ghost 6.0 blog deployment on DigitalOcean Ubuntu 22.04 droplets using Ansible. It delivers a production-ready Ghost blog with comprehensive security, performance optimization, and modern web standards.
- Automated Ghost 6.0 Installation: Latest Ghost version with Node.js 22.x LTS support
- Complete Infrastructure Setup: MySQL database, Nginx reverse proxy, SSL/TLS configuration
- Production Security: SSH hardening, firewall configuration, security headers, and access controls
- Cloudflare Integration: Flexible SSL termination and CDN optimization
- Zero-Touch Deployment: Single-command deployment from fresh droplet to production blog
- Ghost 6.0 Features: Lexical editor, Collections, member recommendations, enhanced performance
- Security-First: SSH key authentication, disabled root login, comprehensive firewall rules
- Modern Stack: Node.js 22.x LTS, MySQL 8.0, Nginx with security headers
- Scalable Architecture: Designed for production workloads with performance optimization
- Maintenance Tools: Automated backup scripts, status monitoring, update procedures
- CMS: Ghost 6.0 (Latest)
- Runtime: Node.js 22.x LTS
- Database: MySQL 8.0
- Web Server: Nginx (Reverse Proxy)
- OS: Ubuntu 22.04 LTS
- Infrastructure: DigitalOcean Droplets
- Automation: Ansible Playbooks
- CDN/Security: Cloudflare (Optional)
- DigitalOcean account with API access (
doctl
configured) - SSH key pair for server access
- Domain name (optional, for production setup)
- Cloudflare account (optional, for CDN/security)
IMPORTANT: This repository uses generic placeholders that must be replaced with your actual values before deployment.
[droplets]
droplet ansible_host=YOUR_SERVER_IP ansible_python_interpreter=/usr/bin/python3 ansible_ssh_private_key_file=~/.ssh/your_private_key
Replace:
YOUR_SERVER_IP
β Your actual server IP address
ADMIN_USER: your_admin_username
SSH_PUBLIC_KEY: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIYourPublicKeyHere your_email@example.com"
SSH_PRIVATE_KEY_PATH: "~/.ssh/your_private_key"
Replace:
your_admin_username
β Your desired admin usernameYourPublicKeyHere
β Your actual SSH public keyyour_email@example.com
β Your email addressyour_private_key
β Your SSH private key filename
GHOST_DOMAIN_NAME: "your-domain.com"
GHOST_URL: "http://your-domain.com"
GHOST_DB_PASSWORD: "your_secure_ghost_password"
GHOST_DB_ROOT_PASSWORD: "your_secure_root_password"
Replace:
your-domain.com
β Your actual domain nameyour_secure_ghost_password
β Strong password for Ghost database useryour_secure_root_password
β Strong password for MySQL root user
Placeholder | Replace With | Found In |
---|---|---|
YOUR_SERVER_IP |
Your droplet IP address | inventory |
your_admin_username |
Your desired admin username | vars.yml , all examples |
your-domain.com |
Your actual domain | ghost-vars.yml , examples |
your_private_key |
Your SSH key filename | vars.yml , inventory |
YourPublicKeyHere |
Your SSH public key | vars.yml |
your_email@example.com |
Your email address | vars.yml |
your_secure_*_password |
Strong passwords | ghost-vars.yml |
- Never commit real credentials to version control
- Use strong passwords (20+ characters) for database credentials
- Verify SSH key permissions are correct (
chmod 600
for private keys) - Test SSH connection before running playbooks
doctl compute droplet list --format ID,Name,PublicIPv4,Status
ssh-keygen -R <old IP>
doctl compute ssh-key list
doctl compute droplet delete your-ghost-droplet
doctl compute droplet create your-ghost-droplet \
--image ubuntu-22-04-x64 \
--size s-1vcpu-2gb \
--region fra1 \
--ssh-keys YOUR_SSH_KEY_FINGERPRINT \
--wait
doctl compute droplet list --format ID,Name,PublicIPv4,Status
sed -i '' 's/<old IP>/<new IP>/' ~/.ssh/config
Host do-droplet
HostName <new IP>
User your_admin_username
IdentityFile ~/.ssh/your_private_key
[droplets]
do-droplet ansible_host=<new IP> ansible_python_interpreter=/usr/bin/python3 ansible_ssh_private_key_file=~/.ssh/your_private_key
Note: These commands connect as
root
user for initial setup
ansible all -i inventory -m ping --private-key ~/.ssh/your_private_key
ansible-playbook -i inventory playbooks/baseline.yml --private-key ~/.ssh/your_private_key
ansible-playbook -i inventory playbooks/mysql.yml --private-key ~/.ssh/your_private_key
ansible-playbook -i inventory playbooks/nodejs.yml --private-key ~/.ssh/your_private_key
ansible-playbook -i inventory playbooks/ghost.yml --private-key ~/.ssh/your_private_key
ansible-playbook -i inventory playbooks/nginx.yml --private-key ~/.ssh/your_private_key
ansible-playbook -i inventory playbooks/firewall.yml --private-key ~/.ssh/your_private_key
ansible-playbook -i inventory playbooks/ssh-security.yml --private-key ~/.ssh/your_private_key
β οΈ WARNING: After SSH hardening, root login is disabled. Use Phase 2 commands below.
Before accessing your site, ensure Cloudflare is properly configured:
- A Record: Point
your-domain.com
to your server IP - Proxy Status: Enable Cloudflare proxy (orange cloud)
- SSL Mode: Set to "Flexible" in Cloudflare SSL/TLS settings
- DNS changes may take up to 24 hours to propagate globally
- Test with different DNS servers:
dig @1.1.1.1 your-domain.com
- For immediate testing, add to
/etc/hosts
:104.21.43.171 your-domain.com
- Ghost uses HTTP backend (configured automatically)
- Cloudflare handles HTTPS termination
- This prevents redirect loops while maintaining security
Once your Ghost blog is deployed, access the admin panel at:
https://your-domain.com/ghost/
Important: Ghost requires manual admin account creation for security reasons. The installation process cannot automatically create admin users.
On your first visit, Ghost will guide you through the setup wizard:
-
Create Admin Account
- Enter your admin email and password
- Choose a strong password for security
- This is the only way to create the initial admin user
-
Configure Blog Settings
- Set your blog title and description
- Configure basic site settings
- Site timezone and language
- Publication details
-
Choose Theme (optional)
- Select from available themes
- Customize site appearance
-
Invite Team Members (optional)
- Add additional authors or editors
- Set user roles and permissions
- π Posts & Pages: Create and manage content
- π¨ Design: Customize themes and site appearance
- βοΈ Settings: Configure site settings and integrations
- π₯ Members: Manage subscribers and memberships
- π Analytics: View site statistics and performance
- π§ Labs: Enable experimental features
This Ghost installation includes comprehensive security measures:
- Rate limiting: 5 requests/min for admin API endpoints
- Sensitive endpoint protection: 3 requests/min for authentication/session/users
- Separate rate limiting zones for different API types
- XSS protection: Comprehensive CSP headers prevent code injection
- Resource restrictions: Controls script, style, image, and font sources
- Applied to all server blocks for complete coverage
- Proxy trust: Proper IP handling behind reverse proxy
- Spam protection: 10-minute minimum wait between failed login attempts
- Built-in CSP: Additional Ghost-level content security
- Cloudflare protection: DDoS mitigation and WAF
- SSH hardening: Key-based auth, disabled root login
- Firewall (UFW): Only necessary ports open
- Database isolation: Separate Ghost user with minimal privileges
- Service isolation: Ghost runs as non-root user
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:
The following security measures are planned for future implementation:
# Restrict Ghost directory permissions
chown -R ghost:ghost /var/www/ghost
chmod -R 750 /var/www/ghost
chmod 640 /var/www/ghost/config.production.json
# Enhanced security logging in Nginx
log_format ghost_security '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time';
access_log /var/log/nginx/ghost_security.log ghost_security;
- Encrypted backups of Ghost content and database
- Offsite storage (separate from main server)
- Regular restore testing
- Automated backup rotation
# Geographic rate limiting
map $geoip_country_code $limit_country {
default "";
CN "country";
RU "country";
}
limit_req_zone $limit_country zone=per_country:1m rate=10r/m;
- Fail2ban configuration for Ghost-specific attacks
- Log monitoring for suspicious patterns
- Automated IP blocking for repeated violations
# Advanced SSL configuration (when not using Cloudflare)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
- File upload restrictions (if uploads enabled)
- Content scanning for malicious code
- Theme/plugin security auditing
- Performance monitoring with alerts
- Security event correlation
- Automated threat response
- GDPR compliance tools
- Audit logging for administrative actions
- Data retention policies
- Priority order: Implement high-priority items first
- Testing required: All changes should be tested in staging
- Documentation: Update this README when implementing
- Automation: Consider Ansible playbooks for repeatable deployment
Note: After SSH hardening, all commands must use
-u your_admin_username
(root login disabled)
ansible all -i inventory -m ping --private-key ~/.ssh/your_private_key -u your_admin_username
ansible all -i inventory -m shell -a "systemctl status ghost_your-domain-com" --private-key ~/.ssh/your_private_key -u your_admin_username
ansible all -i inventory -m shell -a "sudo systemctl restart ghost_your-domain-com" --private-key ~/.ssh/your_private_key -u your_admin_username
ansible all -i inventory -m shell -a "sudo journalctl -u ghost_your-domain-com -f --lines=50" --private-key ~/.ssh/your_private_key -u your_admin_username
ansible all -i inventory -m shell -a "cd /var/www/ghost && ghost version" --private-key ~/.ssh/your_private_key -u your_admin_username
# Stop Ghost service
ansible all -i inventory -m shell -a "sudo systemctl stop ghost_your-domain-com" --private-key ~/.ssh/your_private_key -u your_admin_username
# Update Ghost
ansible all -i inventory -m shell -a "cd /var/www/ghost && ghost update" --private-key ~/.ssh/your_private_key -u your_admin_username
# Start Ghost service
ansible all -i inventory -m shell -a "sudo systemctl start ghost_your-domain-com" --private-key ~/.ssh/your_private_key -u your_admin_username
# Export Ghost data
ansible all -i inventory -m shell -a "cd /var/www/ghost && ghost export" --private-key ~/.ssh/your_private_key -u your_admin_username
# Backup content directory
ansible all -i inventory -m shell -a "sudo tar -czf /tmp/ghost-content-backup-$(date +%Y%m%d).tar.gz /var/www/ghost/content" --private-key ~/.ssh/your_private_key -u your_admin_username
# Download theme to themes directory
ansible all -i inventory -m shell -a "cd /var/www/ghost/content/themes && wget https://github.com/TryGhost/Casper/archive/main.zip" --private-key ~/.ssh/your_private_key -u your_admin_username
# Extract and set permissions
ansible all -i inventory -m shell -a "cd /var/www/ghost/content/themes && unzip main.zip && chown -R your_admin_username:your_admin_username Casper-main" --private-key ~/.ssh/your_private_key -u your_admin_username
# Restart Ghost to recognize new theme
ansible all -i inventory -m shell -a "sudo systemctl restart ghost_your-domain-com" --private-key ~/.ssh/your_private_key -u your_admin_username
- β SSH Key Authentication: Password authentication disabled (VM level)
- β Firewall Protection: UFW configured with minimal open ports (VM level)
- β Domain-Only Access: IP access blocked, redirects to domain (VM level - Nginx)
- β Admin Rate Limiting: Ghost admin interface protected by Nginx rate limiting (Application level)
- β Secure Configuration: Ghost config file permissions restricted to owner only (Application level)
- β Process Isolation: Ghost runs as non-root user with systemd security features (Application level)
- β Database Security: Dedicated Ghost database user with limited privileges (Application level)
- β Content Security: Ghost content directory properly secured (Application level)
- β HTTPS Termination: Cloudflare provides SSL/TLS encryption (External service)
- Strong Admin Password: Use a password manager with 20+ character passwords
- Limited Admin Access: Only log in when necessary, log out immediately
- Regular Updates: Monitor and apply security updates promptly
- Access Logs: Monitor
/var/log/nginx/ghost_access.log
for suspicious activity - Backup Strategy: Regular automated backups of database and content
# Stop Ghost service
ansible all -i inventory -m shell -a "sudo systemctl stop ghost_your-domain-com" --private-key ~/.ssh/your_private_key -u your_admin_username
# Update Ghost
ansible all -i inventory -m shell -a "cd /var/www/ghost && ghost update" --private-key ~/.ssh/your_private_key -u your_admin_username
# Start Ghost service
ansible all -i inventory -m shell -a "sudo systemctl start ghost_your-domain-com" --private-key ~/.ssh/your_private_key -u your_admin_username
# MySQL backup
ansible all -i inventory -m shell -a "mysqldump -u root -pyour_secure_root_password ghost_production | gzip > /tmp/ghost-db-backup-$(date +%Y%m%d-%H%M%S).sql.gz" --private-key ~/.ssh/your_private_key -u your_admin_username
# Content backup
ansible all -i inventory -m shell -a "sudo tar -czf /tmp/ghost-content-backup-$(date +%Y%m%d-%H%M%S).tar.gz /var/www/ghost/content" --private-key ~/.ssh/your_private_key -u your_admin_username
Phase 1 (Fresh Install): ansible-playbook ... --private-key ~/.ssh/your_private_key
Phase 2 (Post-Hardening): ansible ... --private-key ~/.ssh/your_private_key -u your_admin_username
For Ghost operations: ansible ... --private-key ~/.ssh/your_private_key -u your_admin_username
Ghost CLI: Always run Ghost CLI commands from the /var/www/ghost
directory as the your_admin_username
user.
Service Management: Use sudo systemctl
commands for managing the Ghost service.
Example:
# β
CORRECT - Ghost CLI commands
ansible ... -u your_admin_username -m shell -a "cd /var/www/ghost && ghost version"
# β
CORRECT - Service management
ansible ... -u your_admin_username -m shell -a "sudo systemctl restart ghost_your-domain-com"
Re-run playbooks for verification (Ansible idempotency ensures safe re-execution):
# Verify Node.js installation
ansible-playbook -i inventory playbooks/nodejs.yml --private-key ~/.ssh/your_private_key
# Verify MySQL database
ansible-playbook -i inventory playbooks/mysql.yml --private-key ~/.ssh/your_private_key
# Verify Nginx reverse proxy
ansible-playbook -i inventory playbooks/nginx.yml --private-key ~/.ssh/your_private_key
# Verify complete Ghost setup
ansible-playbook -i inventory playbooks/ghost.yml --private-key ~/.ssh/your_private_key
- Ghost Admin Setup: Visit
https://your-domain.com/ghost/
to create your admin account (required manual step) - Blog Frontend: Visit
http://YOUR_SERVER_IP
to see your blog - Service Status: Check
systemctl status ghost_your-domain-com
- Logs: Monitor with
journalctl -u ghost_your-domain-com -f
# 1. Initial setup
ansible-playbook -i inventory playbooks/baseline.yml --private-key ~/.ssh/your_private_key
# 2. Install stack
ansible-playbook -i inventory playbooks/mysql.yml --private-key ~/.ssh/your_private_key
ansible-playbook -i inventory playbooks/nodejs.yml --private-key ~/.ssh/your_private_key
ansible-playbook -i inventory playbooks/ghost.yml --private-key ~/.ssh/your_private_key
ansible-playbook -i inventory playbooks/nginx.yml --private-key ~/.ssh/your_private_key
# 3. Security hardening
ansible-playbook -i inventory playbooks/firewall.yml --private-key ~/.ssh/your_private_key
ansible-playbook -i inventory playbooks/ssh-security.yml --private-key ~/.ssh/your_private_key
# 4. Verify deployment (optional)
ansible-playbook -i inventory playbooks/ghost.yml --private-key ~/.ssh/your_private_key
π Your Ghost blog is ready at http://YOUR_SERVER_IP
!