MAATRIX / Блог / Hugo в Docker Compose: готовый файл

Hugo в Docker Compose: готовый файл

MAATRIX

Hugo — самый быстрый генератор статических сайтов, написан на Go и собирает даже крупный блог за доли секунды. Но у него нет своего Docker-образа от разработчиков, а типовой пример docker run hugo из интернета решает только сборку и ничего не говорит про отдачу готового сайта, пересборку при правке контента и продакшен-конфиг Nginx рядом. Ниже — рабочий docker-compose.yml, который закрывает всё это одним файлом: собирает сайт extended-версией Hugo, автоматически пересобирает его при изменении файлов и отдаёт статику через Nginx.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Почему для Hugo нужна отдельная схема, а не просто образ

Большинство «готовых файлов» в Docker Compose поднимают один долгоживущий процесс — базу, панель, чат. Hugo устроен иначе: это инструмент сборки, а не сервер. Команда hugo --minify один раз превращает Markdown и шаблоны в каталог public с готовыми HTML-файлами и завершает работу. Дальше эти файлы должен отдавать веб-сервер — сам Hugo для продакшен-раздачи не предназначен, хотя и умеет поднять hugo server для предпросмотра.

Отсюда и архитектура компоуз-файла: один контейнер отвечает за сборку (и пересборку при изменениях), второй — за раздачу через Nginx, а связывает их общий volume с каталогом public. Официального образа Hugo от команды проекта нет — есть релизы .deb/.tar.gz на GitHub, поэтому образ надёжнее собрать самим на их основе, а не полагаться на сторонние образы неизвестной свежести.

Dockerfile: extended-версия Hugo с нужными инструментами

Создайте рядом с исходниками сайта файл Dockerfile:

FROM debian:bookworm-slim

ARG HUGO_VERSION=0.139.0

RUN apt-get update && apt-get install -y --no-install-recommends \
        wget ca-certificates git inotify-tools \
    && wget -q "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb" -O /tmp/hugo.deb \
    && apt-get install -y /tmp/hugo.deb \
    && rm /tmp/hugo.deb \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /src

Важные детали:

  • Именно extended-версия (hugo_extended_*.deb) — она умеет обрабатывать SCSS/Sass, который используют многие темы. Обычная сборка на таком шаблоне падает с ошибкой уже на первом hugo build.
  • git нужен, если тема подключена как git-подмодуль (git submodule add) — без него Hugo не сможет её вытянуть при сборке образа или в рантайме.
  • inotify-tools пригодится для автопересборки при изменении контента — об этом ниже, без него пришлось бы пересобирать вручную после каждой правки.
  • Версию HUGO_VERSION замените на актуальную из релизов Hugo на GitHub — свежие версии выходят часто, и фиксировать её явно надёжнее, чем полагаться на latest, которого у .deb-релизов попросту нет.

Если сайт использует тему как подмодуль, добавьте её до сборки образа:

git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke themes/ananke
git submodule update --init --recursive

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Готовый docker-compose.yml

Структура каталогов на сервере:

/opt/hugo-site/
├── site/              # исходники сайта: content, themes, layouts, hugo.toml
├── Dockerfile
├── nginx.conf
└── docker-compose.yml

Сам файл docker-compose.yml:

services:
  hugo:
    build: .
    container_name: hugo-build
    volumes:
      - ./site:/src
      - hugo-public:/src/public
    command: >
      sh -c "hugo --minify --destination /src/public &&
             while inotifywait -r -e modify,create,delete,move,close_write
               /src/content /src/layouts /src/static /src/assets /src/hugo.toml
               2>/dev/null; do
               echo 'Изменения найдены, пересобираю...';
               hugo --minify --destination /src/public;
             done"
    restart: unless-stopped

  nginx:
    image: nginx:1.27-alpine
    container_name: hugo-nginx
    depends_on:
      - hugo
    volumes:
      - hugo-public:/usr/share/nginx/html:ro
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    ports:
      - "80:80"
    restart: unless-stopped

volumes:
  hugo-public:

Логика простая: контейнер hugo при старте собирает сайт один раз, а затем через inotifywait слушает изменения в исходниках и пересобирает public заново при каждой правке — без ручных команд и без CI/CD. Контейнер nginx просто отдаёт то, что лежит в общем volume hugo-public, и ничего не знает про сборку. Запуск:

cd /opt/hugo-site
docker compose up -d --build
docker compose logs -f hugo

В логах hugo вы увидите первую сборку, а затем — сообщения о пересборке при каждом изменении файла в content, layouts, static или assets. Если правите контент прямо на сервере через nano или git pull, обновлённая страница появится на сайте за одну-две секунды без перезапуска контейнеров.

Nginx-конфиг для отдачи готового сайта

Создайте nginx.conf рядом с docker-compose.yml:

server {
    listen 80;
    server_name vashdomen.ru;

    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ $uri/index.html =404;
    }

    error_page 404 /404.html;

    location ~* \.(css|js|svg|woff2?|ttf|png|jpe?g|webp|ico)$ {
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }

    gzip on;
    gzip_types text/plain text/css application/javascript application/json image/svg+xml;
    gzip_min_length 1024;
    gzip_vary on;
}

try_files $uri $uri/ $uri/index.html =404 покрывает и «красивые» URL Hugo вида /blog/post/, где реальный файл лежит по пути /blog/post/index.html. Кастомную страницу 404 Hugo соберёт сам, если в content есть файл 404.md — тогда error_page 404 /404.html покажет её вместо стандартной заглушки Nginx.

Для HTTPS проще всего поставить перед этим Nginx ещё один слой — реверс-прокси с автоматическим SSL. Подробно о настройке связки описано в статьях про Nginx как реверс-прокси и про Caddy с авто-SSL — оба варианта работают, разница в том, что Caddy выпускает и продлевает сертификат сам, без отдельного certbot.

Пересборка без CI/CD: как контент попадает на сервер

Схема с inotifywait из готового файла решает половину задачи — пересобирает сайт, когда файлы в ./site меняются. Вторая половина — как эти файлы вообще оказываются на сервере. Практических варианта два:

  1. Git pull по расписанию. Простой cron на хосте раз в несколько минут тянет изменения из репозитория в ./site, а контейнер hugo сам увидит новые файлы через inotifywait и пересоберёт сайт:
# crontab -e на хосте
*/5 * * * * cd /opt/hugo-site/site && git pull --ff-only >> /var/log/hugo-pull.log 2>&1
  1. Webhook при пуше в репозиторий. Если сайт лежит на GitHub/GitLab, настройте webhook на пуш, который дёргает небольшой скрипт на сервере (например, через webhook — легковесный HTTP-приёмник хуков) с той же командой git pull. Дальше пересборку опять берёт на себя inotifywait внутри контейнера — отдельный CI-раннер не нужен.

Оба варианта проще полноценного пайплайна с GitHub Actions и не требуют доступа контейнеров к Docker-сокету хоста, что снижает риски: контейнер hugo работает только с файлами в volume ./site и никак не может повлиять на остальную систему.

Локальная разработка: hugo server с hot-reload

Продакшен-схема выше пересобирает статику, но не годится для живой разработки темы — там нужен hugo server с автообновлением страницы в браузере при каждой правке. Для этого удобнее отдельный, более простой compose-файл на своей машине или тестовом сервере:

services:
  hugo-dev:
    build: .
    container_name: hugo-dev
    volumes:
      - ./site:/src
    working_dir: /src
    ports:
      - "1313:1313"
    command:
      - hugo
      - server
      - --bind=0.0.0.0
      - --buildDrafts
      - --buildFuture
      - --disableFastRender

Флаг --bind=0.0.0.0 обязателен — без него встроенный сервер слушает только 127.0.0.1 внутри контейнера, и снаружи он будет недоступен даже с проброшенным портом. --buildDrafts и --buildFuture показывают черновики и запланированные посты, которые обычная сборка пропускает. Если открываете превью не с localhost, а через свой домен за реверс-прокси, добавьте --appendPort=false и передайте --baseURL с реальным адресом — иначе LiveReload будет пытаться открыть WebSocket-соединение по неверному порту, и автообновление страницы просто не сработает при рабочей загрузке.

Резервное копирование и обновление версии

Единственное, что реально нужно бэкапить — исходники в ./site: контент, тему, конфиг и Dockerfile. Каталог public внутри named volume hugo-public — производный артефакт, он полностью восстанавливается пересборкой и хранить его отдельно смысла нет. Если ./site уже под git — это и есть ваш бэкап; для дополнительной подстраховки на сервере можно смотреть в сторону restic в Docker Compose, направив его на тот же каталог.

Обновление версии Hugo — правка ARG HUGO_VERSION в Dockerfile и пересборка образа:

docker compose build --no-cache hugo
docker compose up -d

После крупного обновления (смена мажорной версии, например с 0.13x на 0.14x) стоит проверить changelog проекта — иногда меняется поведение шаблонных функций, и тема, которая собиралась без ошибок, может начать падать на новых предупреждениях Hugo, ставших ошибками.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Нужен ли отдельный Nginx-контейнер, если сайт и так статика — нельзя ли отдавать файлы прямо из контейнера Hugo?

Технически можно через hugo server, но это дев-режим: без нормального кэширования статики, gzip и обработки заголовков, которые нужны на продакшене. Разделение на «сборка» и «раздача» — стандартная практика, и Nginx с этим справляется на порядок эффективнее.

Что если тема требует Node.js для сборки CSS (Tailwind, PostCSS)?

Добавьте установку Node в тот же Dockerfile (apt-get install -y nodejs npm) и npm install в исходниках темы перед сборкой Hugo — extended-версия сама Tailwind не соберёт, если тема ожидает отдельный шаг сборки ассетов через package.json.

Пересборка через inotifywait не такая уж лёгкая — не проще ли просто перезапускать контейнер по cron?

Проще, но дороже по задержке: inotifywait реагирует на изменение файла за секунды, а пересборка по cron раз в 5 минут — это до пяти минут задержки публикации правки. Для блога с редкими постами разница не критична, для документации, которая обновляется часто, inotifywait удобнее.

Как быть с несколькими языками сайта (multilingual)?

Ничего специфичного для Docker — конфигурация мультиязычности задаётся в hugo.toml самим Hugo, а собранный public уже содержит все языковые версии по своим путям. Nginx-конфиг из этой статьи их отдаёт без изменений.

Почему не взять готовый образ Hugo из Docker Hub вместо сборки своего?

Официального образа от команды Hugo нет, а сторонние образы часто отстают по версии или собраны не extended-версией. Сборка из .deb-релиза занимает секунды и даёт гарантированно нужную версию — как в примере выше.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →