Something Sneaky...

I explicitly created this project template specifically for this website... So... It kinda works a little bit. Proof is in the pudding or something like that right?

Get Up and Running with Nginx, Let's Encrypt, and Ghost in Less Than 2 Minutes!

Tech Stack:

  • Nginx Reverse Proxy
  • Let's Encrypt SSL Certificates
  • Ghost Headless CMS
  • Docker
  • Docker-Compose

Nginx Reverse-Proxy

Nginx is a lightweight HTTP server widely used as an alternative to Apache, as a reverse-proxy (an external proxy used to safely expose internal resources while introducing a secure layer on top)

Let's Encrypt

Let's Encrypt is a free public SSL Certificate authority and signing service that provide Public/Private KeyPair SSL Certificates used to power TLS encrypted HTTPS connections.

Ghost Headless CMS

Ghost Headless CMS is a lightweight nodeJS based, UX first content management system designed to be enterprise scalable and simplistic to use, full transprency though (pun intended), Ghost is an extremely powerful and customisable. In this architect's opinion, Ghost is in an entirely different league to WordPress.

Docker

Docker is a container virtualisation system that runs natively on Linux, but through a hypervisor virtualised environment in Windows and Mac OSX. Containers allow for single or multiple process isolation with customisable configuration, shared volumns, and internal networking.

Docker-Compose

Docker-Compose is a rudementary single-node container orchestration system whereby Applications are split into services which have internal and external networking by-way-of port exposure and network interfaces.

Git Repository:

GitHub - Foxables/nginx-letsencrypt-mysql8-ghost-template
Contribute to Foxables/nginx-letsencrypt-mysql8-ghost-template development by creating an account on GitHub.

Full Template to Get You Started!

docker-compose.yml File

version: '3.1'

services:
  # Application Server running Ghost Headless CMS.
  ghost:
    image: ghost:5.54.2-alpine
    depends_on:
      - db
    container_name: my-ghost
    env_file:
      - ./example.env
    volumes:
      - ./ghost:/var/lib/ghost/content
      - /etc/localtime:/etc/localtime:ro
    restart: unless-stopped
    networks:
      - backend
      - frontend

  # Database Server.
  db:
    image: mysql:8
    container_name: ghost_db
    environment:
      - MYSQL_ALLOW_EMPTY_PASSWORD=true
    volumes:
      - ./db:/var/lib/mysql
      - /etc/localtime:/etc/localtime:ro
    networks:
      - backend

  # Powers the Reverse-Proxy Automatically.
  nginx-proxy:
    image: nginxproxy/nginx-proxy:latest
    container_name: nginx-proxy
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./vhost:/etc/nginx/vhost.d
      - ./certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - /usr/share/nginx/html
    depends_on:
      - ghost
    networks:
      - frontend
      - default
    restart: unless-stopped

  # Sources TLS Certificates Automatically.
  letsencrypt:
    image: nginxproxy/acme-companion:latest
    container_name: nginx-proxy-le
    environment:
      - DEFAULT_EMAIL=my@email.com
    volumes_from:
      - nginx-proxy
    volumes:
      - ./certs:/etc/nginx/certs:rw
      - ./nginx:/etc/nginx/conf.d
      - /var/run/docker.sock:/var/run/docker.sock:ro
    restart: unless-stopped

networks:
  backend:
    driver: bridge
  frontend:
    driver: bridge

What does this mean?

Holistic View
Let's take a quick moment to examine the above docker-compose file, we have a seperate Frontend and Backend network. The nginx-proxy container can talk to the Ghost container, but not the MySQL container. The Ghost container can talk to the MySQL container, and the Let's Encrypt container cannot talk to any container.

Understand the Networking

In a crappy ASCII diagram, -> denotes a valid connection and -/> denotes an invalid connection.

# From Anywhere
any -/> letsencrypt
any -> nginx-proxy

# From Localhost
localhost -/> mysql
localhost -/> ghost

# Nginx-Proxy Container
nginx-proxy -> world
nginx-proxy -> ghost -> mysql
nginx-proxy -/> mysql

# To World (Internet Egress)
letsencrypt -> world
ghost -> world
mysql -> world

# From World (Internet Ingress)
world -/> mysql
world -/> ghost

As such, we can make the following assertions;

  • Anyone can access the nginx-proxy container.
  • No one can access the letsencrypt container.
  • All containers can access the internet.
  • Only the ghost container can access the mysql container.

Exposing Ports

The nginx-proxy is the only container that exposes ports. Since we are never connecting to the MySQL or Ghost containers directly, we do not need to expose any ports. This is due to the internal docker network setup.

Container Dependency

We use the depends_on property to say that, the nginx-proxy container should not spin up until after the ghost container. This is for our unique setup and it would be a better option to remove the depends_on here, and instead have the ghost container depend_on the MySQL container.

Environment Variables

What's the .envfile? It's a simple key=value pair that enables unique environment configuration that can be applied to each container individually.

How to Set Zone Names

We've provided an example environment variable file in the repo, but for ease of... something... Here's the file;

##############
# Database Configuration.
# Note: The credentials and database must already exist.
##############

database__connection__host=db
database__connection__user=root
database__connection__database=my_database


##############
# The below config can be used to map the container to the Nginx Reverse-Proxy
# and Letsencrypt for Automated SSL Certificate Generation.
##############

# url=http://localhost
# VIRTUAL_HOST=localhost
# VIRTUAL_PORT=2368
# LETSENCRYPT_HOST=localhost

Configuring Hosts

  • The VIRTUAL_HOST variable controls the hostname mapping in nginx.
  • The VIRTUAL_PORT variable controls the port that nginx should connect to the container on when an ingress request is received.
  • The LETSENCRYPT_HOST variable instructs the letsencrypt container to generate an SSL Certificate for the supplied hostname.
  • The url variable instructs the Ghost Instance to map all requests and links to the given value.

It's important to note that there are other configurable values that enable nginx-proxy ingress listening ports and aliases, however, at this stage those are considered out-of-scope for this particular post.

GIT

To the freaking point already!

  1. Clone the repo https://github.com/Foxables/nginx-letsencrypt-mysql8-ghost-template with git clone https://github.com/Foxables/nginx-letsencrypt-mysql8-ghost-template.git.
  2. Add / create your .env file and update the docker-compose.yml file to match.
  3. Run the command $ docker-compose up -d within the locally cloned repository.
  4. Run the command $ docker exec -it ghost /bin/bash, this should open an interactive terminal with your ghost container.
  5. Run the command # mysql -h mysql -u root
  6. Create your database, mysql> create database my_database
  7. Go to the hostname you've configured in your .env file in step 2. You should now see the ghost screen and be guided through a setup.
  8. Eat Cake!

Naturally, by "Eat Cake" I most certainly mean eat some freaking cake, this is mandatory!

Troubleshooting

DNS Resolution

Hostname not found

  • You need to register a valid TLD, ccTLD, or Vanity Domain Name in order to connect to it. Or just change your /etc/hosts file.

Cannot Connect

  • Check if there are any firewalls, if there are, you need to allow inbound connections on ports 443 and 80.
  • Ensure your docker daemon has sufficient networking permissions.

Proxy Error

  • This will likely be an error to do with internal routing. But in some cases it may be that your application's container has crashed and thus there's no route to your container's host.

The Great Conclusion

In my honest opinion, I don't get why people use wordpress anymore. Originally this blog was going to use wordpress, but it just sucked so badly at working with an nginx reverse-proxy. Even on my local computer wordpress was slow and I wasted a few days just trying to get the thing to work how I wanted it to in self-contained docker containers (I didn't want to manage hosting, I just wanted to drop in containers in a stupidly easy setup...)

I had to make manual modifications to the wordpress code to override poorly written hardcoded redirects that prevented wordpress running with a paticular protocol on a specific port behind a simple reverse-proxy using nginx. Why? Because someone decided it was a fantastic idea to hard-code the redirects.
C7B1F07B-21CD-431D-A452-A16680E9B829

To my utter surprise, I was blown away and impressed by the intuitiveness of Ghost. It was almost like love at first sight... So I recommend you try Ghost, even if you're a hardcore Wordpress fanatic.

Up And Running: Nginx + Let's Encrypt + Ghost in Less Than 2 Minutes

Get your website or blog up in a fraction of the time with our tutorial on Ghost with Docker, everything you need to get up and running in as little as 2 minutes using our open source project template.