How to setup Ghost in a VPS using Docker, Mailgun and SQLite

How to setup Ghost in a VPS using Docker, Mailgun and SQLite
Photo by Patrick Fore / Unsplash

Two months ago, I decided to move away from the cloud and self-host all of my services and applications on a single VPS.

You know, just like the cool kids are doing.

In this post, I’ll show you exactly how I set up a Ghost instance from scratch. Here’s what we’ll cover:

By the end of this guide, you’ll have a fully functional, self-hosted Ghost blog.

Have questions or want to share your setup experience? Let me know in the comment section below!

Prerequisites: Prepare Your VPS and Environment

Before we dive into setting up Ghost, let’s make sure we have everything we need.

Here’s what you’ll need:

  • A VPS (Virtual Private Server)
  • DNS Settings
  • Docker installed

If you’re already set, feel free to jump ahead to the next section!

Choose a VPS Provider

Choosing a VPS comes down to personal preference, as pricing and features are similar.

You can’t go wrong with DigitalOcean, Linode, or Vultr—they’re all reliable.

For this guide, I chose Linode’s Dedicated CPU base plan:

  • RAM: 4GB
  • Storage: 80GB
  • CPUs: 2
  • Network In/Out: 40/4 Gbps

Check out their pricing here: Linode Pricing

Prepare DNS Settings

To make your blog accessible, you’ll need to set up a CNAME record in your DNS provider. Here’s an example configuration for a subdomain like 'blog.yourdomain.com':

Name: blog
Type: CNAME
Content: (your VPS IP address)
TTL: 3600

Set Up Docker

Docker helps isolate your Ghost deployment from other services on the VPS.

Here’s how to install Docker on Ubuntu, but if you have any other Linux distribution, check out the docker documentation.

Step 1: Add Docker’s GPG Key

sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

Step 2: Add Docker to Apt Sources

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

Step 3: Install Docker Packages

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Once Docker is set up, you’re ready to deploy Ghost. Let’s dive into the setup!

Setting Up Ghost with Docker

Using Docker to deploy Ghost simplifies the setup process, ensuring easy updates, better isolation, and persistent data storage. Let’s get started!

Step 1: Create a Directory for Ghost Files

We’ll create a dedicated directory to store Ghost content and configuration files.

Run the following commands:

mkdir -p ~/projects/ghost_blog  
cd ~/projects/ghost_blog  

Step 2: Configure Docker Compose for Ghost

Create a docker-compose.yml file in the directory to define the Ghost service:

version: '3.1'

services:
  ghost:
    image: ghost:latest
    container_name: blog_ghost
    restart: always
    ports:
      - "2368:2368"
    environment:
      url: http://blog.mydomain.com
      database__client: sqlite3
      database__connection__filename: /var/lib/ghost/content/data/ghost.db
    volumes:
      - /home/username/projects/ghost_blog:/var/lib/ghost/content

Key Points:

  • Docker Image: The ghost:latest image ensures the most up-to-date version of Ghost.
  • SQLite Database: The database file is stored at /var/lib/ghost/content/data/ghost.db for simplicity.
  • Volumes: Data persistence is ensured by mapping the host directory /home/username/projects/ghost_blog to the container's content directory.

For more details, refer to the Ghost Docker documentation.

Step 3: Start and Verify the Ghost Container

Run the following command to start the Ghost container in the background:

sudo docker compose up -d  

Check the container status with:

sudo docker ps  

Your output should look something like this:

CONTAINER ID   IMAGE                   COMMAND                  CREATED       STATUS       PORTS                                       NAMES
9ecdf7d08764   ghost:latest            "docker-entrypoint.s…"   3 days ago    Up 3 days    0.0.0.0:2368->2368/tcp, :::2368->2368/tcp   blog_ghost

Configuring Nginx

Once Ghost is running on port 2368, the next step is to expose it through a web server like Nginx and secure it with SSL. Nginx acts as a reverse proxy, forwarding traffic from standard web ports (80/443) to Ghost’s internal port (2368) while enabling HTTPS for secure connections.

Step 1: Install Nginx on Your VPS

If Nginx isn’t already installed, use the following commands to install it:

sudo apt install nginx  

Step 2: Create an Nginx Configuration File

We’ll set up Nginx to act as a reverse proxy for Ghost. Create a new configuration file:

sudo vim /etc/nginx/sites-available/ghost_blog  

Add the following configuration:

server {
    listen 80;
    server_name blog.mydomain.com;

    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;
    }
}

Key Points:

  • Proxy: Nginx forwards all traffic to Ghost running on port 2368 using the proxy_pass directive.

Step 3: Enable and Test the Configuration

Enable the configuration by creating a symbolic link:

sudo ln -s /etc/nginx/sites-available/ghost_blog /etc/nginx/sites-enabled/  

Test the configuration for syntax errors:

sudo nginx -t  

Reload Nginx to apply changes:

sudo systemctl reload nginx  

Step 4: Confirm Nginx is Working

Visit your blog in a browser: http://blog.mydomain.com. If everything is set up correctly, your Ghost blog should load.

Troubleshooting Tips:

  • If Nginx doesn’t reload, run sudo nginx -t to check for syntax errors.
  • Ensure the server block file is linked in /etc/nginx/sites-enabled/.
  • Verify that Ghost is running on port 2368 by checking with sudo docker ps.

Setting Up SSL with Let’s Encrypt

SSL encrypts traffic between your blog and visitors, ensuring a secure connection.

Let’s Encrypt offers free SSL certificates, and Certbot simplifies the setup and management process.

Here’s how to configure SSL for your Ghost blog.

Step 1: Install Certbot for SSL Management

Install Certbot and the Nginx plugin to manage SSL certificates:

sudo apt install certbot python3-certbot-nginx  

Step 2: Obtain and Configure SSL Certificates

Run Certbot to obtain SSL certificates and configure Nginx automatically:

sudo certbot --nginx -d blog.mydomain.com  

Certbot will update your Nginx configuration to include SSL settings. The updated configuration should look like this:

nginxCopy codeserver {
    listen 80;
    server_name blog.mydomain.com;

    # Redirect all HTTP traffic to HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name blog.mydomain.com;

    ssl_certificate /etc/letsencrypt/live/blog.mydomain.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/blog.mydomain.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

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

Key Points:

  • HTTP-to-HTTPS Redirection: Ensures all traffic is securely encrypted.
  • SSL Certificate Paths: Certbot manages certificates in /etc/letsencrypt/.
  • Automatic Renewal: Certbot sets up a cron job to renew certificates automatically.

Step 3: Test and Apply SSL Settings

Test the updated Nginx configuration for syntax errors:

sudo nginx -t  

If the test passes, reload Nginx to apply the changes:

sudo systemctl reload nginx  

Step 4: Update Ghost Configuration to HTTPS

Ensure Ghost uses HTTPS by updating the url in the docker-compose.yml file:

environment:
  url: https://blog.mydomain.com

Restart the Ghost service to apply the changes:

sudo docker compose down  
sudo docker compose up -d  

Final Confirmation

Visit your blog at https://blog.mydomain.com to confirm SSL is active.

You should see the secure lock icon in the browser’s address bar.

Congratulations—your blog is now secured with HTTPS!

Configure Mailgun

The last item on our Ghost installation is to set up the bulk mail functionality. This is essential for sending newsletters and handling bulk email campaigns.

We are using Mailgun since Ghost has a nice easy-to-setup built in newsletter delivery feature around it.

Step 1: Create and Verify a Mailgun Account

  1. Create a Mailgun account and verify your email.
  2. Go to Domain Settings under Sending in the Mailgun admin portal.
  3. Add a subdomain for your blog (e.g., mg.mydomain.com). Using a subdomain protects your domain reputation, isolates email types, and simplifies DNS management.

Step 2: Add and Verify DNS Records

After adding your subdomain, Mailgun will display DNS records (e.g., TXT, CNAME, MX). Add these records to your domain registrar's DNS settings and wait for verification.

Why DNS Verification Matters: Authenticating your domain ensures emails are not flagged as spam and boosts deliverability.

Step 3: Create SMTP Credentials

Once your DNS records are verified:

  1. Go to the SMTP credentials tab in the Mailgun admin portal.
  2. Create a new user with a username (e.g., blog@mg.mydomain.com) and a secure password.

Step 4: Configure Ghost to Use Mailgun

Update your docker-compose.yml file with the Mailgun SMTP configuration:

version: '3.1'

services:
  ghost:
    image: ghost:latest
    container_name: blog_ghost
    restart: always
    ports:
      - "2368:2368"
    environment:
      url: http://blog.mydomain.com
      database__client: sqlite3
      database__connection__filename: /var/lib/ghost/content/data/ghost.db
      mail__transport: SMTP
      mail__options__host: smtp.mailgun.org
      mail__options__port: 465  # Use 465 for SSL
      mail__options__secure: true  
      mail__options__auth__user: blog@mg.mydomain.com
      mail__options__auth__pass: your_mailgun_password
      mail__from: 'My Blog <username@mydomain.com>'
    volumes:
      - /home/username/projects/ghost_blog:/var/lib/ghost/content

Key Points:

  • SSL and Port 465: Ensures secure email sending.
  • Environment Variables: Sensitive details like passwords should ideally be stored as environment variables for better security.

Step 5: Test Your Configuration

  1. Restart Ghost to apply the changes:
sudo docker compose down  
sudo docker compose up -d  
  1. Send a test email from Ghost's admin panel (under Settings → Email) to confirm that the Mailgun setup works.

With Mailgun configured, your Ghost blog is ready to send newsletters.

Final Steps

You’re almost there! Open your browser and visit your new site.

To access the Ghost admin portal, simply add /ghost to your domain URL:

yourdomain.com/ghost

The first time you log in, you’ll be prompted to create your admin account. Follow the on-screen instructions, and you’re ready to start managing your blog!

Conclusion

That’s it! Hopefully, if you went trough all of the steps of this process, you will’ have a Ghost app running on your VPS. Now, you can customize your new blog however you want.

In an upcoming guide, we’ll cover how to personalize your site and set up the marketing tools to grow your audience.

If you have any questions or feedback, feel free to reach out—I’d love to hear from you!

P.S. If you enjoyed this guide, don’t forget to subscribe to my newsletter for more tutorials, tips, and updates. 🚀