How to Install Ghost on a VPS with Docker

Ghost has quietly turned into the platform of choice for writers, indie publishers and creators who want to own their audience instead of renting it. It’s quick, it’s focused and it treats newsletters and paid memberships as first-class features rather than bolt-on plugins. Run it on your own VPS and you get full control, predictable costs and plenty of headroom to grow.

Wrap that same install in Docker and life gets easier still. Containers keep Ghost, its database and its dependencies tidy and self-contained. Your host stays clean and upgrades become almost boring. By the end of this guide you’ll have Ghost running in Docker, sitting behind an Nginx reverse proxy, secured with a free Let’s Encrypt certificate and ready for your first post.

What is Ghost?

Ghost is an open-source publishing platform built on Node.js. It launched back in 2013 as a Kickstarter project and it’s now stewarded by the non-profit Ghost Foundation, which means there’s no venture-backed pressure to bolt on features nobody asked for. The whole thing is laser-focused on one job: helping people publish and get paid for their writing.

Where WordPress tries to be everything for everyone, Ghost keeps its scope deliberately narrow. You get a clean, distraction-free editor, native email newsletters, built-in memberships and subscriptions, plus tight Stripe integration for taking payments. It’s fast out of the box, friendly to SEO and happy to run as a headless backend through its Content API if you’d rather bring your own front end. Plenty of established newsrooms and independent creators lean on it every single day.

Why run Ghost in Docker?

You can absolutely install Ghost the traditional way with the Ghost-CLI tool. Docker just makes the whole experience calmer. Instead of installing Node.js, a database and a pile of system packages directly onto your server, you describe everything in a single file and let Docker assemble it for you.

The payoff is real. Your host stays clean because Ghost and MySQL live inside their own containers. Moving to a new server becomes a copy-and-paste job rather than a weekend project. Upgrades are usually a one-line pull. And because the official Ghost image is maintained and tested by the people who build Ghost, you spend less time chasing dependency mismatches and more time writing.

Prerequisites

Ghost is light on its feet, but Docker and a database alongside it still want a little breathing room. Here’s what you’ll want before you start.

Requirement Minimum Recommended
Operating system Ubuntu 22.04 LTS Ubuntu 24.04 LTS
RAM 1 GB 2 GB or more
CPU 1 vCPU 2 vCPU
Storage 10 GB SSD 25 GB SSD or more
Domain name Pointed at your server IP Pointed at your server IP
Access Root or sudo user Root or sudo user

A couple of things to sort out first. Make sure an A record for your domain (and the www version if you want it) points at your VPS public IP address, since Let’s Encrypt needs that in place to issue your certificate. You’ll also want ports 80 and 443 open in your firewall. With that squared away, let’s get building.

Step-by-step installation

Step 1: Update your server

Always start with a fresh system. Log in over SSH and bring everything up to date:

sudo apt update && sudo apt upgrade -y

If the upgrade pulls in a new kernel, go ahead and reboot before you carry on. It only takes a moment and it saves surprises later.

Step 2: Install Docker Engine

You’ll install Docker from its official repository rather than the version in Ubuntu’s default archives, which tends to lag behind. First, grab the few packages apt needs to fetch software over HTTPS:

sudo apt install -y ca-certificates curl gnupg

Next, add Docker’s official GPG key so your system trusts its packages:

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

Now wire up the repository itself:

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

With the repository in place, install everything in one go:

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

By default Docker needs root. To run it as your normal user, add yourself to the docker group:

sudo usermod -aG docker $USER
newgrp docker

Now confirm both pieces are alive and well:

docker --version
docker compose version

Two version numbers staring back at you means Docker is ready to go.

Step 3: Create a project folder and an environment file

Keep things organised by giving your Ghost stack its own home:

mkdir -p ~/ghost && cd ~/ghost

Passwords and other secrets don’t belong hard-coded in your Compose file. Stash them in a separate .env file instead. Create one with your editor of choice:

nano .env

Paste in the following and swap the placeholder values for your own. Use your real domain for the URL and pick strong, unique passwords for the database:

# Public address of your site (use https)
GHOST_URL=https://yourdomain.com

# MySQL credentials
MYSQL_ROOT_PASSWORD=replace-with-a-strong-root-password
MYSQL_DATABASE=ghost
MYSQL_USER=ghost
MYSQL_PASSWORD=replace-with-a-strong-password

Need a hand generating something random and secure? This one-liner spits out a solid password you can paste straight in:

openssl rand -base64 24

Save the file and close the editor. Those values get pulled into your containers in the next step.

Step 4: Write the Docker Compose file

This is the heart of the whole setup. One file describes both containers, how they talk to each other and where their data lives. Create it now:

nano docker-compose.yml

Drop in the following:

services:
  ghost:
    image: ghost:5
    restart: always
    ports:
      - "127.0.0.1:2368:2368"
    environment:
      url: ${GHOST_URL}
      database__client: mysql
      database__connection__host: db
      database__connection__user: ${MYSQL_USER}
      database__connection__password: ${MYSQL_PASSWORD}
      database__connection__database: ${MYSQL_DATABASE}
    volumes:
      - ghost_content:/var/lib/ghost/content
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - ghost_db:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${MYSQL_ROOT_PASSWORD}"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  ghost_content:
  ghost_db:

A few of those lines are doing quiet but important work, so here’s what they mean.

The ports line binds Ghost to 127.0.0.1 rather than 0.0.0.0. That keeps the container reachable only from the server itself, so nobody can hit port 2368 directly from the internet. Nginx will be the single public-facing door, which is exactly what you want.

The depends_on block paired with the database healthcheck solves a classic first-boot race. Without it, Ghost often springs to life before MySQL is ready to accept connections and then crashes in a huff. Here Ghost simply waits until the database reports itself healthy. No race, no crash loop.

The two named volumes, ghost_content and ghost_db, are where your real data lives. Your images, themes and posts sit in one and your database in the other. Because they’re managed by Docker and kept separate from the containers, you can stop, remove or upgrade a container without losing a thing.

One quick note on the image tag. The ghost:5 tag tracks the latest release in the Ghost 5 series, which is current at the time of writing. It’s always worth a glance at the official Ghost image on Docker Hub to confirm the newest major version before you commit.

Step 5: Launch the containers

With both files saved in your ~/ghost folder, bring the whole stack up in the background:

docker compose up -d

Docker now pulls both images, creates your volumes and starts the containers in the right order. The first run takes a minute or two while everything downloads. Check on progress with:

docker compose ps

You’re looking for the database marked as healthy and Ghost showing as running. Want to watch Ghost boot in real time? Tail its logs:

docker compose logs -f ghost

Press Ctrl + C to stop following the logs once you’ve seen Ghost report that it’s listening. The app is now alive on the server, but only locally for now. Time to give it a proper front door.

Step 6: Install and configure Nginx as a reverse proxy

Nginx sits in front of Ghost, takes requests on ports 80 and 443 and quietly forwards them to the container on port 2368. Install it first:

sudo apt install -y nginx

Now create a server block for your site. Swap yourdomain.com for your real domain throughout:

sudo nano /etc/nginx/sites-available/ghost.conf

Paste in this configuration:

server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1: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;
    }

    client_max_body_size 50m;
}

Those proxy_set_header lines matter more than they look. They pass the visitor’s real host and IP through to Ghost, and the X-Forwarded-Proto header tells Ghost the original request came in over HTTPS. The client_max_body_size line lifts the upload ceiling to 50 MB so larger images and theme zips don’t bounce off a default limit.

Enable the site by linking it into sites-enabled, then drop the default placeholder so it doesn’t get in the way:

sudo ln -s /etc/nginx/sites-available/ghost.conf /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default

Test the configuration for typos before you reload, since a broken config can take Nginx down:

sudo nginx -t

If it reports the syntax is okay, reload Nginx to apply everything:

sudo systemctl reload nginx

Visit http://yourdomain.com in your browser and you should see the default Ghost site. It’s plain HTTP for the moment, so let’s fix that next.

Step 7: Secure your site with a free SSL certificate

Running a site over plain HTTP in 2026 isn’t an option, and Ghost expects HTTPS anyway. Certbot fetches a free certificate from Let’s Encrypt and configures Nginx for you in a single command. Install it through snap, which is the method the Certbot team recommends:

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Now ask Certbot to sort out your certificate. Use the same domain names you put in your Nginx config:

sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot asks for an email address for renewal reminders and gets you to agree to the terms of service. When it offers to redirect HTTP traffic to HTTPS, say yes. It then rewrites your Nginx config, loads the certificate and reloads the server.

Reload https://yourdomain.com and you’ll see the padlock. Let’s Encrypt certificates last 90 days, but the snap package sets up automatic renewal in the background so you can forget about it. Want to be sure the renewal works? Run a dry run:

sudo certbot renew --dry-run

No errors means renewals will tick over on their own from here on.

Step 8: Create your admin account

One step left and it’s the fun one. Head to your Ghost admin area by adding /ghost to your domain:

https://yourdomain.com/ghost

The setup screen walks you through naming your site and creating the owner account with your email and a password. Once that’s done you land in the Ghost dashboard, where you can pick a theme, connect a custom domain for newsletters, wire up Stripe for memberships and start writing. Your install is live and fully yours.

Keeping Ghost up to date

One of the real joys of the Docker approach shows up at update time. Because your data lives in named volumes rather than inside the containers, updating Ghost is mostly a matter of pulling a fresh image and recreating the container. Your posts, images and settings stay put.

Before any update though, take a backup. It only takes a second and it’s cheap insurance. From inside your ~/ghost folder, snapshot the database:

docker compose exec db sh -c 'exec mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" ghost' > ghost-backup-$(date +%F).sql

That writes a dated SQL dump into your current folder. With a backup safely in hand, pull the latest image and recreate the stack:

docker compose pull
docker compose up -d

Docker spots the newer image, gracefully replaces the running Ghost container and leaves your volumes untouched. Tidy up any old, unused images afterwards to reclaim disk space:

docker image prune

A quick word of caution on the database. The mysql:8.0 tag deliberately pins you to the 8.0 series. Jumping MySQL across major versions needs more care than a simple pull, so leave that tag alone unless you’ve read the upgrade notes and taken a fresh backup.

Troubleshooting common issues

Even tidy setups hit the odd snag. Here are the hiccups that trip people up most often with a Dockerised Ghost install, plus how to sort each one out.

The Ghost container keeps restarting

Nine times out of ten this means Ghost can’t reach its database. Start by reading the logs, since they usually name the culprit outright:

docker compose logs ghost

Check that the database container shows as healthy in docker compose ps. Then make sure the database values in your .env file match on both sides. A typo in MYSQL_PASSWORD is a surprisingly common offender. If MySQL simply needed more time to wake up on first boot, the healthcheck should now have it sorted on the next restart.

Nginx returns a 502 Bad Gateway

A 502 is Nginx admitting it can’t reach Ghost behind the scenes. First, confirm the Ghost container is actually running with docker compose ps. If it’s up, double-check that the proxy_pass address in your Nginx config points at http://127.0.0.1:2368 and that the port matches the binding in your Compose file. A fresh sudo nginx -t followed by sudo systemctl reload nginx often clears it once the container is healthy.

Database access denied, even with the right password

Here’s a sneaky one. MySQL only reads those MYSQL_USER and MYSQL_PASSWORD values the very first time it initialises an empty data volume. Change a password in .env after that and the running database happily ignores it, because the old credentials are already baked into the volume.

If you’re still setting things up and have nothing worth keeping, the cleanest fix is to wipe the volume and start fresh:

docker compose down -v
docker compose up -d

Be warned: the -v flag deletes your database volume and everything in it. Only reach for this on a fresh install with no content you care about. On a live site, change the password from within MySQL itself instead.

Certbot fails to issue a certificate

Certbot validation almost always falls over for one of two reasons. Either your domain’s A record isn’t pointing at the server yet, or a firewall is blocking port 80. Confirm your DNS has propagated and resolves to the right IP, then make sure port 80 is open:

sudo ufw allow 'Nginx Full'

DNS changes can take a little while to spread across the internet. If you’ve only just pointed your domain, give it some time and try Certbot again shortly after.

Image uploads fail with a 413 error

A 413 means the file you’re uploading is bigger than Nginx is willing to accept. The fix is the client_max_body_size directive in your server block, which this guide already set to 50 MB. If you’re still hitting the wall on large theme zips or images, bump that number higher, save the file, then test and reload Nginx:

sudo nginx -t
sudo systemctl reload nginx

Where to go from here

Ghost is live, secured and ready for words. Here are a few sensible next moves to make the most of it.

  • Set up email. Connect a transactional mail provider like Mailgun so Ghost can send newsletters and member sign-in links reliably.
  • Turn on memberships. Hook up Stripe in the admin settings to start offering free and paid subscriptions to your readers.
  • Pick a theme. Browse the official Ghost marketplace or build your own to give your publication a look that’s unmistakably yours.
  • Automate your backups. Wrap that mysqldump command in a cron job and ship the output somewhere off the server for real peace of mind.
  • Lock down SSH. Disable password logins in favour of SSH keys to keep your server that bit safer.

You’ve built something solid here. A containerised Ghost install behind Nginx with automatic SSL is the same architecture plenty of professional publications run on, and you now understand every moving part of it. The hard work is done, so go and publish something worth reading.

The post How to Install Ghost on a VPS with Docker appeared first on GreenGeeks.

版权声明:
作者:lichengxin
链接:https://www.techfm.club/p/236881.html
来源:TechFM
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>