Migrating Ghost to Docker

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

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

Stanislas
Author
Stanislas
I like building things with code and computers

Comments

20Atom feed
Markdown supported
  1. Normcore

    Not Docker related but I cannot use the x25519 in my Ghost configuration. I donʼt manage to restart nginx… how did you achieve that? Thanks for the help.

    1. StanislasAuthor

      Hey, show me this part of your conf + your nginx and openssl version so that I can help you

    2. Normcore

      Ubuntu 16.03 with OpenSSL 1.1.1-pre9 (beta) and Nginx 1.14 stable version.

      My ssl-params.conf :

      ssl_protocols TLSv1.2;ssl_prefer_server_ciphers on;ssl_ciphers EECDH+AESGCM:EECDH+CHACHA20:EECDH+AES!AES128;ssl_ecdh_curve secp384r1;ssl_session_cache shared:SSL:10m;ssl_session_tickets off;ssl_stapling on;ssl_stapling_verify on;resolver 80.67.169.12 80.67.169.40 valid=300s;resolver_timeout 5s;add_header Strict-Transport-Security 'max-age=155552000; includeSubDomains; preload';add_header X-Frame-Options SAMEORIGIN;add_header X-Content-Type-Options nosniff;

      If I change secp384r1 to x26619, I get SSL_CTX_set1_curves_list("x25519") failed (SSL:) when I try to restart nginx.

      I used the ec-384 keylength when issueing my certificate with letsencrypt, should I reissue a certificate with a different keylength?

    3. Normcore

      Same error even with caps :( SSL_CTX_set1_curves_list("X25519") failed (SSL:)

    4. Normcore

      Can I use your script nginx-autoinstall to reinstall my nginx?

    5. Normcore

      Since my blog is still empty, I have decided to start again from cratch with Ubuntu 18.04 (now officially supported by Ghost) instead of 16.04. I used your script to install the Nginx stable version, with LibreSSSL and no extra modules. Here is the nginx -V output now:

      nginx version: nginx/1.14.0 built by gcc 7.3.0 (Ubuntu 7.3.0-16ubuntu3) built with LibreSSL 2.7.4 TLS SNI support enabled configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --user=nginx --group=nginx --with-cc-opt=-Wno-deprecated-declarations --without-http_ssi_module --without-http_scgi_module --without-http_uwsgi_module --without-http_geo_module --without-http_split_clients_module --without-http_memcached_module --without-http_empty_gif_module --without-http_browser_module --with-threads --with-file-aio --with-http_ssl_module --with-http_v2_module --with-http_mp4_module --with-http_auth_request_module --with-http_slice_module --with-http_stub_status_module --with-http_realip_module --with-openssl=/usr/local/src/nginx/modules/libressl-2.7.4

      I have one issue though: the Ghost-CLI does not detect the Nginx installation when I use your script. I got this message:

      System checks failed with message: 'Missing package(s): nginx'

      Notes: if I install Nginx with a regular apt-get, the Ghost-CLI detects nginx successully!

      Do you have any idea why?

      Though I am not yet familiar with Docker, and considering I am restarting with a brand new setup, maybe I should install Ghost with Docker instead?

    6. Normcore

      So the explanation is: Ghost-CLI checks nginx with dpkg -l | grep nginx.

    7. Normcore

      Sorry for my Ghost problems! I managed to fix everything. It was actually an Ubuntu 16.04 issue. With Ubuntu 18.04 and a simple apt-get install nginx, the X25519 curve works just fine. Still, I prefer to use your nginx autoinstall sh, because it is nice to build it with LibreSSL (are you planning to update with 2.8.0, it is not clear whether or not it is a stable version). Unfortunately the Ghost-Cli does not detect nginx, so I skipped the nginx setup to do it manually. Really simple! Problem solved, now. I gave up on Docker for the moment, too hard for me :-) Feel free to delete my off-topic comments.

  2. Normcore

    Angristan, I am considering migrating to Docker. Do I need to have Node and SQLite installed in my Ubuntu server or does it come already included with the Ghost image? After I have saved my /ghost/content folder and /ghost/config.production.json file, do I move them into /var/lib/ghost/ after the creation of the container? Thanks for the help!

    1. StanislasAuthor

      No, this is the point of Docker, everything is inside the image. You will have to use volumes.

  3. Ozy

    Hi Angristan,

    How do you manage the cache ? (docker side or with a RP on the host)

  4. Anonymous

    Is it anyhow possible that the content you're mounting updates in the frontend? I added the content as a volume. There is no cache activated, but i still doesn't updates, whenever i change something. Please help.

  5. OOIII

    the step "chown -R 1000:1000 content/ config.production.json" is necessary?

    1. Skippy

      yes, as otherwise the guest docker image can't read or write to it, and it won't work. - on Linux (I don't think Windows manages permissions the same way).