Jekyll, Docker, and Nginx Reverse Proxy for Static Blogs

1 September 2018

I have no self control, so last week I bough https://www.reenginee.red/. I’m probably going to use it as a home for some sustainable energy/micro-grid/open hardware projects.

In any case, I thought I would use Jekyll to host a fairly unchanging static site. I’d found a theme I really liked, and some projects I’ve seen lately (https://sustywp.com/) have made me more aware of the burden a really heavy site can have on the internet, and on users that don’t have a great connection. (Spoiler: Reenginee.red is currently sitting at about 250kb of transfer, most of that Javascript and CSS, as opposed to the 3mb of the front page of this blog).

Here’s what I started out with:

Docker config

Here’s the docker-compose.yml the Jekyll Docker image recommends:

Recomended Jekyll docker-compose.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
version: "3"
services:
  site:
    command: jekyll serve
    image: jekyll/jekyll:latest
    volumes:
      - $PWD:/srv/jekyll
      - $PWD/vendor/bundle:/usr/local/bundle
    ports:
      - 4000:4000
      - 35729:35729
      - 3000:3000
      - 80:4000

What are all the ports for? Digging into the Jekyll docs, it’s fairly simple to find that:

  • 4000 is the default port that Jekyll uses to serve pages via the jekyll serve command
  • 35729 is the port used by livereload, which you may or may not use? I’m not using it.
  • 3000? IDK
  • 80 is just remapping port 4000 externally.

nginx-proxy takes care of all the port issues, though, and uses internal networks to route the reverse-proxy HTTP traffic, so we don’t actually need to expose anything but 35729. We do need to tell nginx-proxy that we’re running on port 4000, though, and not port 80.

I had problems with jekyll simply returning 0 if I tried to cache gems. So I had to remove the gem caching directory. I couldn’t find any bug reports for this issue, and cached gems aren’t necessary (they speed up the initial load time of the container). Later, I discovered that in order for relative URLs to resolve properly, you must run jekyll in “production” mode (default mode is “development”), so I also added an environment variable to bring up jekyll in “production” mode. The final docker-compose.yml looks something like this:

Final working docker-compose.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
version: "3"
services:
    nginx-proxy:
        image: jwilder/nginx-proxy
        ports:
            - "80:80"
            - "443:443"
        volumes:
            - /PATHTO/certs:/etc/nginx/certs:ro
            - /PATHTO/vhost.d:/etc/nginx/vhost.d
            - /PATHTO/html:/usr/share/nginx/html
            - /var/run/docker.sock:/tmp/docker.sock:ro
        labels:
            - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy"
    nginx-letsencrypt:
        image: jrcs/letsencrypt-nginx-proxy-companion
        volumes_from:
            - nginx-proxy
        volumes:
            - /PATHTO/certs:/etc/nginx/certs:rw
            - /var/run/docker.sock:/var/run/docker.sock:ro
    reengineered:
            command: jekyll serve
            image: jekyll/jekyll:latest
            volumes:
                  - /PATHTO/srv:/srv/jekyll
            environment:
                    - JEKYLL_ENV=production
                    - VIRTUAL_HOST=reenginee.red,www.reenginee.red
                    - VIRTUAL_PORT=4000
                    - LETSENCRYPT_HOST=reenginee.red,www.reenginee.red
                    - LETSENCRYPT_EMAIL=[YOUREMAILHERE]
            ports:
                  - "35729:35729"
            networks:
                  - default

The config sections for nginx-proxy and nginx-letsencrypt are really, really, really exactly from the tutorials for those two containers. They work really incredibly well, and I can’t recommend those two packages enough.

They key things are to include, in each proxied container:

  • VIRTUAL_HOST=reenginee.red,www.reenginee.red
    • This is a comma separated list of what URLs nginx will proxy. This means any incoming request for reenginee.red or www.reenginee.red will go to this container.
  • LETSENCRYPT_HOST=reenginee.red,www.reenginee.red
    • Same, but for pulling Let’s Encrypt certificates
  • LETSENCRYPT_EMAIL=[YOUREMAILHERE]
    • Email address for bothering you if Let’s Encrypt fails for some reason. I haven’t had a failure yet, so I actually don’t know how well this function works!

Jekyll Bring-up

Okay, home free, right?

The jekyll docs and the docs included with the jekyll docker container are pretty unclear about bringing up a new site. The instructions read:

docker-compose run site jekyll new mysite, which works. But jekyll won’t, by default, be able to get to the generated _config.yml, so when you run docker-compose exec site jekyll build later,  you’ll get a bunch of errors, and a half-built site. Additionally, the half-built site will live at https://[YOURURL]/mysite, and you PROBABLY want it to live at https://[YOURURL]/.

What you really want to run is docker-compose run site jekyll new . which creates a new site at the root directory.

Now you end up with a _config.yml file at the /PATHTO/srv/ directory configured in the docker-compose.yml:

Default Jekyll _config.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# Welcome to Jekyll!
#
# This config file is meant for settings that affect your whole blog, values
# which you are expected to set up once and rarely edit after that. If you find
# yourself editing this file very often, consider using Jekyll's data files
# feature for the data you need to update frequently.
#
# For technical reasons, this file is *NOT* reloaded automatically when you use
# 'bundle exec jekyll serve'. If you change this file, please restart the server process.

# Site settings
# These are used to personalize your new site. If you look in the HTML files,
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
# You can create any custom variable you would like, and they will be accessible
# in the templates via {{ site.myvariable }}.
title: Your awesome title
email: your-email@example.com
description: >- # this means to ignore newlines until "baseurl:"
  Write an awesome description for your new site here. You can edit this
  line in _config.yml. It will appear in your document head meta (for
  Google search results) and in your feed.xml site description.
baseurl: "" # the subpath of your site, e.g. /blog
url: "" # the base hostname & protocol for your site, e.g. http://example.com
twitter_username: jekyllrb
github_username:  jekyll

# Build settings
markdown: kramdown
theme: minima
plugins:
  - jekyll-feed

# Exclude from processing.
# The following items will not be processed, by default. Create a custom list
# to override the default setting.
# exclude:
#   - Gemfile
#   - Gemfile.lock
#   - node_modules
#   - vendor/bundle/
#   - vendor/cache/
#   - vendor/gems/
#   - vendor/ruby/

Most importantly, configure the “url” to the correct url, and you should be off to the races, and caught up with all the other Jekyll documentation that lives on the internet!

Any time you change your _config.yml, you’ll have to restart the jekyll container (docker-compose restart [containername]), but otherwise, changes will auto-refresh!