Migrating Ghost to Docker

Table of Contents
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.