Hugo — довольно удобная штука. За 5 минут можно поднять себе бложик на локалхосте. Потом, конечно, придётся потратить ещё пару часов, чтобы настроить деплой этого дела на сервер и раздачу оттуда по HTTPS.

Здесь я вкратце расскажу то, как я это сделал у себя для этого конкретного блога.

Я не стану описывать что такое Hugo, Docker, Nginx, Linux, клавиатура, пальцы и т.д. Подразумевается, что читатель знает обо всём этом и минимально умеет пользоваться.

Итак, что же мы хотим получить в итоге: статичный блог, раздаваемый Nginx’ом по HTTPS, который автоматически выкатывается на сервер. Можно было бы катить на Github/Gitlab Pages, но об этом напишет кто-нибудь другой.

Собственно сам процесс достаточно простой. Я пропущу шаги с созданием нового блога, выбором оформления и написанием статей. Сразу к сборке и деплою.

Я использую Gitlab и его CI сервис. Так что пишем в .gitlab-ci.yml:

image: docker

stages:
  - build

build_image:
  stage: build
  services:
    - docker:dind
  only:
    - tags
  script:
    - docker build -t $CI_REGISTRY_IMAGE:latest -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
    - docker push $CI_REGISTRY_IMAGE:latest

Затем в Dockerfile:

FROM registry.gitlab.com/pages/hugo:latest AS builder

WORKDIR /opt/blog
COPY . /opt/blog
RUN hugo


FROM nginx:stable
# билд блога закидывается в папку, откуда nginx по-умолчанию раздаёт статику
# поэтому не нужно производить никаких манипуляций с конфигами
COPY --from=builder /opt/blog/public /usr/share/nginx/html

И ещё нужно создать docker-compose.yml:

version: '3.7'

services:
  blog:
    image: registry.gitlab.com/borodyadka/blog:latest
    ports:
      - 127.0.0.1:8080:80
    restart: always

Здесь я пробрасываю порт 80 из контейнера наружу как 127.0.0.1:8080. Он слушает локалхост специально, чтобы снаружи нельзя было зайти напрямую в контейнерный Nginx. А висит не на 80 порту, т.к. снаружи у меня стоит ещё один Nginx, который терминирует SSL и обслуживает ещё несколько сайтов, каждый из которых также висит не на 80 или 443 порту, а на 808x.

SSL

Получение SSL сертификата от Let’s Encrypt — дело довольно простое. Есть множество утилит, которые реализуют ACME протокол и позволяют получить сертификат парой команд. Я использую Acme.SH внутри Docker контейнера.

Acme.SH позволяет получить сертификат, используя DNS challenge без ручных манипуляций с DNS записями, достаточно передать ему API токен провайдера и название самого провайдера. В моём случае это DigitalOcean.

Я создал файл renew.sh, который из cron’а будет вызываться раз в месяц и обновлять все нужные мне сертификаты:

#!/bin/bash

export DO_API_KEY="123-do-key"

docker run --rm -it \
  -v /etc/nginx/ssl:/acme.sh  \
  --net=host \
  -e DO_API_KEY=${DO_API_KEY} \
  neilpang/acme.sh --issue --dns dns_dgon -d borodyadka.wtf -d www.borodyadka.wtf

service nginx reload

После успешного завершения, новые сертификаты окажутся в /etc/nginx/ssl на хост-машине, откуда их прочитает Nginx и сможет использовать.

Есть отличный конфиг для Nginx, который на SSL-тестах выдаёт результат A+, именно его я взял за основу своего:

server {
  listen 443 ssl http2;
  index index.html;
  server_name borodyadka.wtf;

  ssl_certificate /etc/nginx/ssl/borodyadka.wtf/fullchain.cer;
  ssl_certificate_key /etc/nginx/ssl/borodyadka.wtf/borodyadka.wtf.key;

  # https://gist.github.com/nrollr/9a39bb636a820fb97eec2ed85e473d38
  # Improve HTTPS performance with session resumption
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 5m;

  # Enable server-side protection against BEAST attacks
  ssl_prefer_server_ciphers on;
  ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

  # Disable SSLv3
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

  # Enable HSTS (https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security)
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

  # Enable OCSP stapling (http://blog.mozilla.org/security/2013/07/29/ocsp-stapling-in-firefox)
  ssl_stapling on;
  ssl_stapling_verify on;
  ssl_trusted_certificate /etc/nginx/ssl/borodyadka.wtf/fullchain.cer;

  location / {
    proxy_pass http://localhost:8080;
  }
}

server {
  listen 80;
  listen [::]:80;
  root /var/www/html;

  index index.html index.htm index.nginx-debian.html;
  server_name borodyadka.wtf;

  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

  return 301 https://$host$request_uri;
}

Автоматическое обновление

Я люблю максимально всё автоматизировать. В данном случае мне не хочется делать действий больше, чем git push --tags, чтобы увидеть обновление в блоге.

Немного подумав я пришел к такой схеме:

  1. ставим тег в git;
  2. пушим тег в origin;
  3. CI собирает по тегу новый образ;
  4. с сервера по крону раз в час опрашивается Docker Registry Gitlab’а;
  5. если прилетел новый образ, то он выкатывается.

Скрипт для редеплоя получился достаточно простым:

#!/bin/bash

CURRENT=$(docker inspect --format={{.Config.Image}} registry.gitlab.com/borodyadka/blog:latest)
docker pull registry.gitlab.com/borodyadka/blog:latest
LATEST=$(docker inspect --format={{.Config.Image}} registry.gitlab.com/borodyadka/blog:latest)

if [ "$CURRENT" != "$LATEST" ]; then
        cd $(dirname $0)
        docker-compose up -d
fi

Я добавил его в cron с интервалом в 1 час.

На этом всё.