/ Ghost

Migrating Ghost to Docker

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.

I recommend you to learn Docker and to know what you're doing before moving your precious website to a container. I also assume that Docker is up and running on your server to follow the tutorial.

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 angristan.xyz www.angristan.xyz;
	return 301 https://angristan.xyz$request_uri;

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

        if ($host = www.angristan.xyz) {
                return 301 https://angristan.xyz$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.

Next step: doing my own Ghost image!

Header image source

Angristan

Angristan

I'm an 18 years old French sysadmin studying at a IT school and working for a web hosting company.

Read More