Hey y'all. Since you're reading this, you found my shiny new blog, which took me some time to setup in my environment. That's why you will find a tutorial here on how to: Setup ghost with MySQL in Docker on a Ubuntu 18.04 running Apache2 as webserver and with letsencrypt as SSL certificate authority.

I can only stress that it took me some time to set this up, but once it was running I completely fell in love with this blog software. I am so genuinely happy with it, I enjoy writing much more than before. But now, without further ado. Let's get started.


For this tutorial you will need to know how to access a server through SSH (check this tutorial if you need help). We'll start off with a pre-installed Ubuntu 18.04, because that's what you can easily get over at hetzner.de for a pretty decent starting price of 2,96 € per month. Upload your SSH key to the web-interface and you are good to go. Also you should check that the DNS' A and AAAA records for your domain point to the server.

Setting up Docker

Installing Docker on Ubuntu 18.04 is as easy as

sudo apt update
sudo apt install docker.io

Then we need to start and enable the systemd unit so docker always runs, even after a reboot

sudo systemctl start docker
sudo systemctl enable docker

Finally we want to make our lives a little bit easier when dealing with docker and install docker-compose

sudo apt install docker-compose

If we want to run docker from an unprivileged user, we can just add that user to the docker group with the following command. We will, however, use the root user to set things up.

sudo usermod -aG docker username

Setting up the blog

Running docker containers for ghost and MySQL

The following part assumes we are logged in as root user (or ran `sudo -i`). If something is not working you probably don't have the correct permissions.

Create a file called docker-compose.yml in the directory where your blog data will live e.g. /opt/mypersonalblog

mkdir -p /opt/mypersonalblog
cd /opt/mypersonalblog
touch docker-compose.yaml

Now open the file with an editor of your choice (for example nano) and copy the following contents into the file. We'll go through the file layout in a second.

version: "3"

    image: ghost:2.0
      - mysql
      url: "https://plantprogrammer.de/"
      database__client: "mysql"
      database__connection__host: "mysql"
      database__connection__port: 3306
      database__connection__user: "root"
      database__connection__password: "supersecretpassword"
      database__connection__database: "ghost"
      server__port: "2368"
      - intranet-ghost
    restart: unless-stopped
      - ./blog:/var/lib/ghost/content
      - 2368:2368
    image: mysql:5.7
      MYSQL_ROOT_PASSWORD: "supersecretpassword"
      - intranet-ghost
    restart: unless-stopped
      - ./db:/var/lib/mysql


Line 1 just describes the docker compose version to use, as they are not always compatible
Line 4 - 23 describe the ghost blog container
Line 25-33 describe a database container running MySQL
Line 35-36 describe a network interface on which the two can talk

To create your blog, we will adjust a few settings. In the environment section of the ghost container you should modify the url entry to resemble your domain. You should furthermore change the database password to your liking in lines 14 and 28. The volumes entries map a local (before colon) directory to a directory inside the container (after colon). Since they are relative paths we can leave them be. Last but not least, there is a ports section in the ghost definition. This entry determines which host port (before colon) will map to a specific port in the container (after colon). We will leave it at 2368, but we have to remember this port for later.

At the very first run, we need to start the containers twice. In the first run, the database is created and the blog tries to access it, before it is finished creating. So we run the containers once, abort the process with Ctrl+C and restart the containers in the background

docker-compose up
# Wait for 30 seconds and press Ctrl+C
docker-compose up -d

Configuring apache to serve the blog

First we need to activate a bunch of apache2 modules

a2enmod ssl rewrite proxy proxy_http headers

Next we create a new apache config that redirects all HTTP traffic to secure HTTPS and serves whatever is running on port 2368 in the HTTPS part of the configuration. Note, the internal communication between apache and docker is not encrypted, but the proxy passes on the https headers. Create a file in /etc/apache2/sites-available/ and call it something like 050-mypersonalblog.conf

<VirtualHost *:80>
        ServerName plantprogrammer.de
        ServerAlias www.plantprogrammer.de
        RewriteEngine on
        RewriteCond %{SERVER_NAME} =plantprogrammer.de [OR]
        RewriteCond %{SERVER_NAME} =www.plantprogrammer.de
        RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]

<VirtualHost *:443>
        ServerName plantprogrammer.de
        ServerAlias www.plantprogrammer.de

        ServerAdmin info@plantprogrammer.de

        ErrorLog ${APACHE_LOG_DIR}/blog_error.log
        CustomLog ${APACHE_LOG_DIR}/blog_access.log combined
        SSLEngine On
        ProxyPreserveHost On
        ProxyRequests Off
        ProxyPass / http://localhost:2368/
        ProxyPassReverse / http://localhost:2368/
        RequestHeader set X-Forwarded-Proto "https"
        RequestHeader set X-Forwarded-Port "443"
        SSLCertificateFile /etc/ssl/private/ssl-cert-snakeoil.key
        SSLCertificateKeyFile /etc/ssl/certs/ssl-cert-snakeoil.pem

Of course, you have to change all occurrences of plantprogrammer.de to your desired domain name. If this is done, it is time to test the installation. We enable the site and reload our webserver

a2ensite 050-mypersonalblog.conf
systemctl reload apache2

NB: When I first tried this, I always received errors about too many redirects. That is because I hadn't passed the https-headers on to the container. So if you get this error, you might either be lacking the RequestHeader lines in your apache config or you have not enabled the headers module.

If everything worked, we can fire up a browser and go to our domain. At the moment we are seeing a warning that the connection is not secure and we'll deal with that in a moment. First, we are greeted by a default blog layout with a few example posts.

Go to https://<your_domain>/ghost Here you can register the first (admin) user. After this initial setup step, whenever you go to /ghost, you will end up in the backend. Here you have several options to organize publishing (first section) and a bunch of settings to change the appearance and functionality of your new blog :)

Setting up SSL Certificates with LetsEncrypt

By far the easiest way to setup letsencrypt certificates is a small python software called certbot. First we install certbot and then we run it on all domains setup in the apache config

apt install certbot
certbot -d plantprogrammer.de
# Can take multiple domains like so
# certbot -d plantprogrammer.de,plantprogrammer.com

Congratulations you can start blogging now. If you want to check out other themes make sure to check out https://ghost.org/marketplace/ for free as well as paid themes. If you want to learn more about Ghost check out the docs: https://ghost.org/docs/

If you found this helpful, want to share your own blog, or have questions, don't hesitate contacting me on twitter (https://twitter.com/plantprogrammer) my DMs are open.