Keep your server clean and move your Ghost blog to a Docker container!

In my first post I said I installed Ghost with ghost-cli, the classic way. I did also say that I wanted to run it in Docker but that I didn’t know Docker enough to do it. In fact, I tried to set up Ghost in Docker a few times while being bored at school, but I didn’t succeed, so it ended up like it is now.

For the past week though, I’ve been learning and using Docker a lot, and finally moved a dozen services into containers.

And I succeeded! It was really simple when I understood what I was doing.

ghost-cli, the software to manage your Ghost installation, is great and it’s way simpler to install Ghost that it was a few years ago. However, it’s still not completely straightforward and I has some issues with permissions.

Also, managing Node.js versions and NPM modules on your server can be a little messy, whereas right now everything is in my Docker container and I can trash it and rebuild it whenever I want while keeping my server clean (that’s the point of Docker).

Another advantage is that it is way easier to use a SQlite database, as ghost-cli requires MySQL

Backup

Save your /ghost/content folder and /ghost/config.production.json file.

If you’re using a MySQL database, make a dump.

You can now stop the Ghost service to free the port.

Docker Compose

We’ll use docker-compose to manager our Ghost container using a simple Yaml file.

Here is the one I use:

version: "3.1"

services:
  ghost:
    container_name: ghost
    image: ghost:1.21.3-alpine
    restart: always
    ports:
      - 127.0.0.1:2368:2368
    volumes:
      - ./content:/var/lib/ghost/content
      - ./config.production.json:/var/lib/ghost/config.production.json
  • We use the offcial Alpine image (change the version)
  • We bind the port 2368 of the container to 127.0.0.1:2368 on our host
  • We mount our content folder that we previously backed up
  • We mount our configuration file that we previsouly backed up

Example if you use MySQL:

version: "3.1"

services:
  mysql:
    container_name: ghost_mysql
    image: mariadb:10.3
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example

  ghost:
    container_name: ghost
    image: ghost:1.21.3-alpine
    restart: always
    depends_on: mysql
    ports:
      - 127.0.0.1:2368:2368
    volumes:
      - ./content:/var/lib/ghost/content
      - ./config.production.json:/var/lib/ghost/config.production.json
    environment:
      database__client: mysql
      database__connection__host: mysql
      database__connection__user: root
      database__connection__password: example
      database__connection__database: ghost

Make sure the content folder and config.production.json are in your present directory.

Then apply the correct permissions:

chown -R 1000:1000 content/ config.production.json

1000 being the UID and GID of the ghost user inside the container.

Now you can run:

docker-compose up -d

And your container will come to life.

If you want to import your MySQL dump:

docker exec -i ghost_mysql mysql -u root -p ghost < dump.sql

Updating Ghost

It won’t be a pain anymore!

Change your version number in your docker-compose.yml.

Fetch the latest images:

docker-compose pull

And restart the containers if needed:

docker-compose up -d

… that’s all.

Reverse proxy

You can use a reverse proxy the exact same way as you were before, without modifying a single file.

For you information, this is ~what I use:

server {
  listen 80;
  listen [::]:80;
  server_name stanislas.blog www.stanislas.blog;
  return 301 https://stanislas.blog$request_uri;

  access_log /dev/null;
  error_log /dev/null;
}
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name stanislas.blog www.stanislas.blog;

  if ($host = www.stanislas.blog) {
    return 301 https://stanislas.blog$request_uri;
  }

  access_log /var/log/nginx/ghost-access.log;
  error_log /var/log/nginx/ghost-error.log;

  ssl_certificate /etc/nginx/https/fullchain.pem;
  ssl_certificate_key /etc/nginx/https/key.pem;

  ssl_protocols TLSv1.2;
  ssl_ecdh_curve X25519:P-521:P-384:P-256;
  ssl_ciphers EECDH+CHACHA20:EECDH+AESGCM:EECDH+AES;
  ssl_prefer_server_ciphers on;
  ssl_stapling on;
  ssl_stapling_verify on;
  resolver_timeout 5s;
  ssl_session_cache shared:SSL:10m;
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";

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

The best setup being having Nginx in a container and adding it to your docker-compose stack.

Enjoy

I’m really enjoying my new Ghost Docker container! It’s still early to give you feedback on how it runs in the long-term, but so far it has been incredibly easy to move my Ghost website to Docker, even more so because I use SQLite! I’m glad I don’t have to deal with NPM anymore.

Header image source