Hugo в Docker Compose: готовый файл
Hugo — самый быстрый генератор статических сайтов, написан на Go и собирает даже крупный блог за доли секунды. Но у него нет своего Docker-образа от разработчиков, а типовой пример docker run hugo из интернета решает только сборку и ничего не говорит про отдачу готового сайта, пересборку при правке контента и продакшен-конфиг Nginx рядом. Ниже — рабочий docker-compose.yml, который закрывает всё это одним файлом: собирает сайт extended-версией Hugo, автоматически пересобирает его при изменении файлов и отдаёт статику через Nginx.
Содержание
- Почему для Hugo нужна отдельная схема, а не просто образ
- Dockerfile: extended-версия Hugo с нужными инструментами
- Готовый docker-compose.yml
- Nginx-конфиг для отдачи готового сайта
- Пересборка без CI/CD: как контент попадает на сервер
- Локальная разработка: hugo server с hot-reload
- Резервное копирование и обновление версии
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество 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 меняются. Вторая половина — как эти файлы вообще оказываются на сервере. Практических варианта два:
- 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
- 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 ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →