August 13, 2019

Ghost, NGINX and Docker on a Pi

Running Ghost and NGINX on Docker running on a Raspberry Pi

Finally, we're here.

This post is the last of a series explaining how to setup Ghost, NGINX and Docker on a Raspberry Pi. You can find the rest of the posts here. This particular post is the culminating event of the series - where we actually get Ghost, NGINX and Docker running all together on your pi. By now, you should already have all of the prerequisite work done and now you're simply making use of Docker running on your Pi.

For that matter - since we're just using docker now, the contents of this post would actually work on any system running docker.

This post has two parts. A Local Ghost Setup - how to run Ghost generically and locally and a far more useful Domain Ghost Setup that talks about what I did to make the blog that you're currently reading work - along with certificates and all.

All of this with the hope that I'm not actually just writing a guide on how to hack my blog - this is not an invitation.


How this works:

                                                    .─────────.
┌───────────────────┐                   .──The Internet─.      `.
│                   │▓              _.─'                 `──.    )
│                   ■─────443─────────────────────┐          ╲,─'`.
│      Browser      │▓            (               │           )    )
│                   │▓             `.             │         ,'  ,─'
│                   ■─────80─────────────────┐    │     _.─'───'
└───────────────────┘▓                   `───┼────┼────'
 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                       │    │
                                             │    │
┌────────────────────────────────────────────┼────┼────────────────┐
│                                            │    │                │▓
│                                            │    │                │▓
│  ┌─────────────────────────────────────────┼────┼───────────┐    │▓
│  │                                         │    │           │░   │▓
│  │                                         │    │           │░   │▓
│  │                                         │    │           │░   │▓
│  │      ┌───────────────┐          ┌───────■────■──┐        │░   │▓
│  │      │               │░         │               │░       │░   │▓
│  │      │    GhostJS    ■───2368───■     NGINX     │░       │░   │▓
│  │      │               │░         │               │░       │░   │▓
│  │      └────────────■──┘░         └──────────■────┘░       │░   │▓
│  │       ░░░░░░░░░░░░│░░░░          ░░░░░░░░░░│░░░░░░       │░   │▓
│  │                   │                        │             │░   │▓
│  │                   │                        │             │░   │▓
│  │                   │                        │             │░   │▓
│  │  Docker           │                        │             │░   │▓
│  │                   │                        │             │░   │▓
│  └───────────────────┼────────────────────────┼─────────────┘░   │▓
│   ░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░   │▓
│                      │                        │                  │▓
│                  ┌───■──────────────┐         │                  │▓
│                  │                  │░   ┌────■─────────────┐    │▓
│                  │    Persistent    │░   │                  │░   │▓
│                  │     Storage      │░   │   Certificates   │░   │▓
│                  │                  │░   │                  │░   │▓
│                  └──────────────────┘░   └──────────────────┘░   │▓
│  Raspberry Pi     ░░░░░░░░░░░░░░░░░░░░    ░░░░░░░░░░░░░░░░░░░░   │▓
│                                                                  │▓
└──────────────────────────────────────────────────────────────────┘▓
 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
 
 
 

If you're here for the "simple" version i.e. without certificates - you can ignore that part of the diagram above - and just stop half way through this tutorial.


Preparation

It's really quite simple - Regardless if you're doing the Local Ghost Setup or the Domain Ghost Setup we really only need to make a few files and folders for this entire project.

The setup for this is super simple, and once completed the tree structure of your project directory will look like this:

ghost
├── content
│   └── (This is the persistent storage for ghost)
├── docker-compose.yml
└── nginx
    ├── Dockerfile
    ├── default.conf
    └── html

First, let's make a few directories, and touch a few files.

mkdir -p ghost/content ghost/nginx/html
touch ghost/docker-compose.yml ghost/nginx/Dockerfile ghost/nginx/default.conf

Now if you run tree ghost you should get something that looks like this:

tree ghost
ghost
├── content
├── docker-compose.yml
└── nginx
    ├── Dockerfile
    ├── default.conf
    └── html
    
3 directories, 3 files

All together now, this looks like:

mkdir -p ghost/content ghost/nginx/html
touch ghost/docker-compose.yml ghost/nginx/Dockerfile ghost/nginx/default.conf
tree ghost
ghost
├── content
├── docker-compose.yml
└── nginx
    ├── Dockerfile
    ├── default.conf
    └── html

3 directories, 3 files

Now, we need to put some goodies inside of the docker-compose.yml, nginx/Dockerfile and nxginx/default.conf.

The Dockerfile stays true he same betwene the Local Ghost Setup and the Domain Ghost Setup - it's the other two files which are slightly different.

So, since it's the same, let's go ahead and add the two lines to it that we need.

Edit nginx/Dockerfile and put the following inside:

FROM nginx:latest

COPY default.conf /etc/nginx/conf.d

... I know, super simple right? Basically we're pulling nginx:latest to build our image from, and simply adding this config file to it. We probably could have achieved this by volume mounting the file - but for reasons that escape me at the time of this writing there was a reason that this was a "better way". If you happen to come across that, please comment in the comment section down below and I'll be happy to edit this section. If I run across that in the future I'll come back here and edit this section.


Local Ghost Setup Local Ghost

Edit your docker-compose.yml file and put this in it:

version: '3.3'
services:

  ghost:
    image: ghost:2.25.4-alpine
    volumes:
      - ./content:/var/lib/ghost/content

  nginx:
    build:
      context: ./nginx
      dockerfile: Dockerfile
    depends_on:
      - ghost
    ports:
      - "80:80"

This is a super simple Docker Compose file. We've got two "services" or "apps" that will be run when we docker-compose up - ghost and nginx.

For ghost we're telling it which specific image we want to use, and setting a volume mount so that your blog will be persistent when you "reboot" your docker container. For example, let's say you lose power to your pi, because we've mounted this "external to the container" directory from the file system on your pi - when you get power again and run the container it will mount all of the content you need to run your blog from this volume - it will be like nothing happened.

for nginx we're setting a build context, defining a Dockerfile - launching the nginx contanier after the Ghost container PID 1 has signal, telling docker to bind port 80 from the raspberry pi to the nginx container.

Now, edit your nginx/default.conf and put this inside:

server {
  listen 80;
  server_name localhost;
  client_max_body_size 20M;

  location / {
    proxy_pass http://ghost:2368;
  }
}

This is pretty simple here. We're telling nginx to listen on port 80. We set the server_name to localhost and telling nginx to accept uploads of files up to 20MB - useful for when you want to upload photos, etc. through the Ghost content editor. The last thing we do is tell nginx to basically pass port 80 traffic through to the ghost container running on port 2368.

Simple, easy pleasy.


Domain Ghost Setup Production Ghost

I'm going to have to come up with a better name for this section because - yeah, "Domain Ghost Setup" just sounds stupid. And it's not really as descriptive as it could be for what we're trying to do here.

Basically - if you're only trying to run Ghost locally for testing, etc. The above "Local Host Setup" will do just fine.

But if you want to actually run a blog on a Raspberry Pi, and actually serve traffic from it in a production worthy way, then you're going to need a bit more than what the "Local Host Setup" is going to give you.

Edit the two files and put the required contents inside:

docker-compose.yml

version: '3.3'
services:

  ghost:
    image: ghost:2.25.4-alpine
    restart: always
    environment:
      url: https://www.uberbuilder.com
    volumes:
      - ./content:/var/lib/ghost/content

  nginx:
    build:
      context: ./nginx
      dockerfile: Dockerfile
    restart: always
    depends_on:
      - ghost
    ports:
      - "80:80"
      - "443:443"
    volumes:
       - /etc/letsencrypt/:/etc/letsencrypt/
      - ./nginx/html:/usr/share/nginx/html

Differential between Local Ghost and Production Ghost

Local Ghost                          Production Ghost

version: '3.3'                       version: '3.3'
services:                            services:

  ghost:                               ghost:
    image: ghost:2.25.4-alpine           image: ghost:2.25.4-alpine
    ─────────────[new]────────────────▶  restart: always
    ─────────────[new]────────────────▶  environment:
    ─────────────[new]────────────────▶    url: https://www.uberbuilder.com
    volumes:                             volumes:
      - ./content:/var/lib/ghost/content   - ./content:/var/lib/ghost/content

  nginx:                               nginx:
    build:                               build:
      context: ./nginx                     context: ./nginx
      dockerfile: Dockerfile               dockerfile: Dockerfile
    ─────────────[new]────────────────▶  restart: always
    depends_on:                          depends_on:
      - ghost                              - ghost
    ports:                               ports:
      - "80:80"                            - "80:80"
    ─────────────[new]────────────────▶    - "443:443"
    ─────────────[new]────────────────▶  volumes:
    ─────────────[new]────────────────▶    - /etc/letsencrypt/:/etc/letsencrypt/
    ─────────────[new]────────────────▶    - ./nginx/html:/usr/share/nginx/html

The difference here now, is restarting and encryption.

  • restart: always Now we will have the docker containers automatically restart should there be any issues with them - and if the Raspberry Pi loses power and comes back - once the Docker Daemon is alive again the containers will start doing their thing. (It's like ultra tiny mini kubernetes orchistration - the lite version)
  • `443, and the volume mounts for the certificate location and directory to serve up acme challenges on port 80 for when you renew your certificates with Let's encrypt.

nginx/default.conf

server {
  listen 80;
  listen [::]:80;
  server_name uberbuilder.com www.uberbuilder.com;
  # Useful for Let's Encrypt
  location /.well-known/acme-challenge/ { root /usr/share/nginx/html; allow all; }
  location / { return 301 https://$host$request_uri; }
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name uberbuilder.com www.uberbuilder.com;
  client_max_body_size 20M;

  ssl_protocols TLSv1.2;
  ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;

  ssl_certificate     /etc/letsencrypt/live/uberbuilder.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/uberbuilder.com/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://ghost:2368;
  }
}

Okay, so I'm not going to give you a diff from the local to this version because - there is simply too much to compare as the "local" version was super silly simple.

Basically, for port 80 we want to do two things. If this is a acme challenge from Let's encrypt - than we need to point that request to the static directory that we've mounted from the docker-compose.yml file in all other cases - redirect to https, or port 443.

For port 443, we list out all of the needful items for SSL to work, and then pass through all requests to Ghost running on port 2368.

So, only NGINX worries about the certs and SSL - Ghost is totally oblivious to all of this happening and simply provides the blog on port 2368.


Conclusion

I'm landing in a plane right now... I'll have to come back later and wrap this up.

Long story short - HURRAY, congratulations! you now have Ghost and NGINX running on Docker secured with Let's encrypt certificates. You're awesome! :)