I was recently fiddling with Ghost, and I liked it so much I set up this blog using it. Seems like it would be fitting to write an article on how I approached it.

I am working a lot with containers, so instead of the conventional approach, this will be a quick and easy setup using Docker in Swarm mode.

We will be running 4 services. Nginx as the reverse proxy, Ghost as the CMS, MySQL as the database and PHPMyAdmin as a GUI for our database - just for convenience's sake.

Prerequisites:

  • Docker
  • SSL certs (if you don't have them, check out this post to create them for free under 1 min, then continue where you left off)
  • Basic familiarity with Nginx

Here's what we're going to do:

  • Initiate Swarm (in case you haven't done it yet)
  • Create an Overlay network that our services are going to join
  • Create the MySQL service
  • Create the PHPMyAdmin service
  • Create the Ghost service
  • Build and create the Nginx service
  • Profit

Alright here we go.

Initiate Docker Swarm

Initiating Swarm is the easiest thing ever, just type this in the terminal and hit enter

docker swarm init

Now, depending on your host, you might get some error complaining about --advertise-addr, but don't worry about it, just type the following

docker swarm init --advertise-addr <HOST_IP_ADDRESS>

Cool, let's move on

Create an Overlay network

I'm usually naming my custom networks all caps, so I can find it easier, but you do you.

docker network create -d overlay --attachable <NETWORK_NAME_HERE>

TIP: check to see if it's created using the docker network ls command

Create the Database service

Super simple stuff here. We're going to name it, specify the network to join (that we just created), specify a root password, and create a bind mount (if you want persistent data, that is)

Change the snippet with your own values and paste it into your terminal.

docker service create \
--name db \
--replicas 1 \
--network <NETWORK_NAME_HERE> \
--env MYSQL_ROOT_PASSWORD=<YOUR_ROOT_PASSWORD_HERE> \
--mount type=bind,source=<DIRECTORY_ON_HOST>,destination=/var/lib/mysql \
mysql:5.7

The <DIRECTORY_ON_HOST> can be whatever directory you choose to persist your MySQL data in. Just make sure you're always binding the same directory when you're creating the database service.

Create the Database GUI

We need to pay extra attention to provide the same root password that we specified when creating the database.

docker service create \
  --name phpmyadmin \
  --replicas 1 \
  --network <NETWORK_NAME_HERE> \
  --env PMA_HOST=db \
  --env MYSQL_ROOT_PASSWORD=<YOUR_ROOT_PASSWORD_HERE> \
  --publish 8080:80 \
  phpmyadmin/phpmyadmin

Should be good to go, you can check if it works at http://<HOST_IP_ADDRESS>:8080, the login credentials are root and the root password you specified.

Create the Ghost service

When creating the ghost service we'll need to set up a couple environment variables, and make a bind mount of the content folder to persist the data.

Should be super simple, just change the values and paste it into the terminal as before.

docker service create \
  --name blog \
  --replicas 1 \
  --network <NETWORK_NAME_HERE> \
  --env url=https://<YOUR_WEBSITE_DOMAIN> \
  --env database__client=mysql \
  --env database__connection__host=db \
  --env database__connection__user=root \
  --env database__connection__password=<YOUR_ROOT_PASSWORD_HERE> \
  --env database__connection__database=ghost \
  --mount type=bind,source=<DIRECTORY_ON_HOST>,destination=/var/lib/ghost/content \
  ghost

Create the Proxy service

Lastly, on to the Nginx setup.

I am assuming you have your SSL certs and your dhparam file ready to roll. If not, like I said in the introduction above, please check out this article and get your hands on the SSL certs.

Disclaimer: I like to set up my Proxy service so that any number of services can be added and substracted from it easily in the future. This requires some additional configuration starting out, but much easier deployment in the long term.

So, by the time we're done here, we are expecting to have 1 nginx directory, that contains 4 files: Dockerfile, default.conf, nginx.conf, and website.conf

I am going to assume you know how to create directories and files, so I'll spare you of the commands associated with those; instead, I'll list the contents of the files below

Dockerfile content

# Base image
FROM nginx

# Copy configs
COPY nginx.conf /etc/nginx/nginx.conf
COPY default.conf /etc/nginx/conf.d/default.conf
COPY website.conf /etc/nginx/conf.d/website.conf

# Expose ports
EXPOSE 80
EXPOSE 443

# Start command
CMD ["nginx", "-g", "daemon off;"]

default.conf content (make sure to substitute example.com with your domain)

server {
    listen      80 default_server;
    listen [::]:80 default_server;
    server_name example.com *.example.com;

    location / {
        #rewrite ^ https://$host$request_uri? permanent;
        return 301 https://$host$request_uri;
    }

    #for certbot challenges (renewal process)
    location ~ /.well-known/acme-challenge {
        allow all;
        root /data/letsencrypt;
    }
}

nginx.conf content (this contains a bunch of configurations for performance and whatnot, don't worry about it - the important part is the line that says include configs)

worker_processes auto;

error_log /var/log/nginx/error.log crit;

events { worker_connections 1024; }

http {
  access_log off;

  sendfile on;

  tcp_nopush on;
  tcp_nodelay on;

  gzip on;
  gzip_min_length 10240;
  gzip_comp_level 1;
  gzip_vary on;
  gzip_disable msie6;
  gzip_proxied expired no-cache no-store private auth;
  gzip_types
        # text/html is always compressed by HttpGzipModule
        text/css
        text/javascript
        text/xml
        text/plain
        text/x-component
        application/javascript
        application/x-javascript
        application/json
        application/xml
        application/rss+xml
        application/atom+xml
        font/truetype
        font/opentype
        application/vnd.ms-fontobject
        image/svg+xml;


  reset_timedout_connection on;

  include /etc/nginx/mime.types;


  # Include servers
  include /etc/nginx/conf.d/*.conf;
}

website.conf content (we're redirecting the naked domain to www here and setting up the server - make sure you substitute <YOUR_DOMAIN> with your actual domain)

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name <YOUR_DOMAIN>;

    server_tokens off;

    ssl_certificate /etc/letsencrypt/live/<YOUR_DOMAIN>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<YOUR_DOMAIN>/privkey.pem;

    ssl_buffer_size 8k;

    ssl_dhparam /etc/ssl/certs/dhparam-2048.pem;

    ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
    ssl_prefer_server_ciphers on;

    ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

    ssl_ecdh_curve secp384r1;
    ssl_session_tickets off;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8;

    return 301 https://www.<YOUR_DOMAIN>$request_uri;
}


server {
    server_name www.<YOUR_DOMAIN>;
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_tokens off;

    ssl_buffer_size 8k;
    ssl_dhparam /etc/ssl/certs/dhparam-2048.pem;

    ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

    ssl_ecdh_curve secp384r1;
    ssl_session_tickets off;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4;

    ssl_certificate /etc/letsencrypt/live/<YOUR_DOMAIN>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<YOUR_DOMAIN>/privkey.pem;

    
    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
        proxy_pass http://blog:2368;
    }

}

Now that we have our folder with the 4 files, we're going to build a Docker image, by executing the command below (making sure we're in the folder where the Dockerfile is located)

docker build -t proxy .

Alright good stuff, now if we'll list all the images with docker image ls, we will see our image created.

Finally, let's release our Proxy service to the swarm

docker service create \
--name proxy \
--replicas 1 \
--network <NETWORK_NAME_HERE> \
--publish 80:80 \
--publish 443:443 \
--mount type=bind,source=/docker-volumes/nginx/configs,destination=/etc/nginx/conf.d \
--mount type=bind,source=/docker-volumes/dh-param/dhparam-2048.pem,destination=/etc/ssl/certs/dhparam-2048.pem \
--mount type=bind,source=/docker-volumes/etc/letsencrypt/live/<YOUR_DOMAIN>/fullchain.pem,destination=/etc/letsencrypt/live/<YOUR_DOMAIN>/fullchain.pem \
--mount type=bind,source=/docker-volumes/etc/letsencrypt/live/<YOUR_DOMAIN>/privkey.pem,destination=/etc/letsencrypt/live/<YOUR_DOMAIN>/privkey.pem \
proxy

PRO Tip: Whenever making changes to the Nginx configurations, or re-deploying the Ghost service, make sure to reload the configurations using the command below

docker container exec <NGINX_CONTAINER_ID> nginx -s reload

That's all folks, enjoy your new blog!

If you ever run into an ECONNRESET issue with Docker Swarm, check out this article to see how to easily fix it!