2. Hugo on nginx in Docker container

2021-07-03 docker nginx hugo blog-story

Motivation

In the last post I described how to set up blog using Hugo engine. When I make some changes (add a new post for example) I need to:

  1. generate static files with hugo command and copy them to server,
  2. move static files to directory managed by nginx,
  3. reload nginx.

Now I will introduce Docker containers. After Docker containerization, release instruction will be:

  1. create image of new blog version and copy it to server,
  2. load copied docker image,
  3. stop container of previous image version and start container of new image version.

Is it simpler? Not really - there are still 3 steps. So why do this? One can argue it extracts responsibility of nginx installation and configuration to Docker. It is true, but I am going to use also standalone nginx as reverse proxy, so some out-of-docker configuration will be needed. In my case incoming requests will be handled like:

	     +------------------------Server-----------------------------+
	     |  +--------------------+	    +-----Docker container----+  |
request ---> |  |nginx(reverse proxy)| ---> | nginx ---> static files |  |
	     |  +--------------------+      +-------------------------+  |
	     +-----------------------------------------------------------+

And without docker container it looks like:

	     +-------------------Server-------------------+
	     |  +--------------------+	                  |
request ---> |  |nginx(reverse proxy)| ---> static files  |
	     |  +--------------------+      	          |
	     +--------------------------------------------+

Configuration required on host machine is not simpler in the first case. I will use docker because:

  1. I have two small servers. Using Docker I can simply configure two independently working blog instances accessible on the same HTTP address. In case of failure one of them, second still will be work.
  2. If I would like to host different services I already have a unified interface to service management. It is not important if service is a java application or if it is a set of static files served by HTTP server like nginx. I can still manage them using Docker tool.
  3. I get simple monitoring out of the box (docker stats command).

I would like to show you how to release service with Docker and prepare simply release process based only on docker command and shell scripts. Despite this, future works should cover activities like configuration of docker registry or CI/CD tools.

Dockerfile

FROM alpine:3.14
ENV REFRESHED_AT 20210624

RUN apk update && apk add tcpdump nano tzdata && cp /usr/share/zoneinfo/Europe/Warsaw /etc/localtime && echo "Europe/Warsaw" > /etc/timezone && apk del tzdata && adduser --no-create-home --disabled-password --gecos "" --uid 2727 wheelfred wheelfred
ADD --chown=wheelfred:wheelfred public.zip /var/www/
RUN apk add nginx && unzip /var/www/public.zip -d /var/www && rm -rf /var/www/public.zip && chown -R wheelfred:wheelfred /etc/nginx /var/log/nginx /var/lib/nginx /var/www /run/nginx
ADD --chown=wheelfred:wheelfred blog-nginx.conf /etc/nginx/http.d/

USER wheelfred
WORKDIR /etc/nginx
VOLUME ["/var/log/nginx"]

ENTRYPOINT ["nginx", "-g", "daemon off;"]

At the beginning I set appropriate timezone (“Europe/Warsaw”) and I created user wheelfred with uid 2727 which will run nginx process. I specified uid directly because thanks to this I can set ownership of volume directories in the host system to 2727 and have certainty that user in docker container wouldn’t have permission issues.

Next, I added public.zip host file to /var/www/ container directory. public.zip contains generated by hugo command static files. /var/www/ directory will be used by nginx to serve static files.

Second RUN command unzips generated static files to /var/www/public container directory and adds ownership of few directories to wheelfred user so it can start nginx without permission issues.

Second ADD command adds blog-nginx.conf file. It is simple nginx configuration file, which points that nginx container will listen on 8080 port and it will serve file from location /var/www/public/index.html:

server {

  listen                8080;
  root                  /var/www/public;

  proxy_set_header      Host $host;
  proxy_set_header      X-Real-IP $remote_addr;
  proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header      Proxy "";

  location / {
    index index.html;
  }
}

In the end I changed user to wheelfred, set working directory to nginx directory (/etc/nginx) and I add volume to keeps nginx logs. I start nginx with -g daemon off; option to run process in foreground (otherwise container will stop immediately after start).

Building an image

To simplify release process I added an auxiliary script to prepare public.zip file, build docker image, save it to tar.gz file and print command to copy image file to server. Pay attention that I used git tags to tag docker images.

#!/bin/bash

hugo
zip -r docker/public.zip public/*
cd docker
docker build -t="stepniewski.tech/blog:`git describe --tags`" .

echo "Saving docker image to tar.gz file..."
docker save stepniewski.tech/blog:`git describe --tags` | gzip -c > target/blog-`git describe --tags`.tar.gz

echo "Image ready. To copy it paste cmd:"
echo -e "scp docker/target/blog-`git describe --tags`.tar.gz $MY_SERVER"

I keep described Dockerfile in project under docker subdirectory. It saves image file (.tar.gz archive) to docker/target subdirectory, so it is good idea to create .dockerignore file which will exclude the target directory from Docker build context.

Launching the container

On the server side copied docker image file need to be loaded:

docker load -i /home/wheely/blog-v1.0.0.tar.gz

and started:

docker run \
  --name blog \
  -p 127.0.0.1:10003:8080 \
  -v $SERVICES_PATH/blog/volumes/log:/var/log/nginx \
  -d stepniewski.tech/blog:v1.0.0

To simplify I extracted above command to shell script and I keep it in service root directory. It starts container on 127.0.0.1 interface, it is because I use reverse proxy nginx as described at the beginning. To make blog-service accessible from external, I need to add configuration to reverse-proxy similar to:

server {

  server_name           stepniewski.tech;

# HEADERS

  location = / {
    return 307 https://stepniewski.tech/blog/;
  }

  location ~ ^/blog/(.*)$ {
    proxy_pass http://127.0.0.1:10003/$1;
  }

# SSL CONFFIGURATION...

And it’s all. Now blog is accessible under https://stepniewski.tech/blog and https://stepniewski.tech urls.