Сайт сделан при помощи Astro. Статьи лежат рядом с кодом в Markdown, локальная разработка запускается в Docker с hot reload, а production-контейнер раздаёт готовые HTML-файлы через nginx.
Ниже собраны файлы, на которых держатся локальная разработка, сборка и публикация статьи. Их можно взять за основу для другого Astro-проекта.
Как проходит запрос
Во время сборки Astro читает страницы, компоненты и Markdown-файлы, а затем записывает результат в каталог dist/. В production не нужен Node.js: nginx отдаёт уже собранные HTML, CSS и JavaScript.
Markdown + Astro components
│
▼
npm run build
│
▼
dist/ directory
│
▼
nginx :80
Node.js остаётся только на этапах разработки и сборки. В запущенном production-контейнере работает nginx.
Структура проекта
В проекте нет отдельной CMS. Контент, маршруты и инфраструктура находятся в одном репозитории:
.
├── src/
│ ├── components/ # Astro-компоненты страниц
│ ├── content/
│ │ └── writing/
│ │ ├── ru/ # статьи на русском
│ │ └── en/ # статьи на английском
│ ├── layouts/ # общий HTML-каркас
│ ├── pages/ # маршруты Astro
│ ├── styles/ # глобальные стили
│ └── content.config.ts # схема frontmatter
├── public/ # файлы без обработки Astro
├── tests/e2e/ # Playwright-тесты
├── Dockerfile # production-сборка
├── Dockerfile.dev # разработка с HMR
├── docker-compose.yml # локальные сервисы
├── Makefile # короткие команды
└── package.json
Astro создаёт маршруты из src/pages/, а статьи загружает через Content Collections. Схема в src/content.config.ts проверяет обязательные поля до сборки, поэтому статья с ошибкой во frontmatter не попадёт в релиз незаметно.
Production Dockerfile
Production-образ удобно собирать в два этапа. Первый этап устанавливает зависимости и запускает Astro, второй содержит только nginx и каталог dist/.
# syntax=docker/dockerfile:1.7
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY astro.config.mjs tsconfig.json ./
COPY src ./src
COPY public ./public
ARG SITE_URL=https://example.com
ENV SITE_URL=${SITE_URL} \
ASTRO_TELEMETRY_DISABLED=1
RUN npm run build
FROM nginx:alpine AS runtime
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY /app/dist /usr/share/nginx/html
EXPOSE 80
HEALTHCHECK \
CMD wget --quiet --tries=1 --spider http://127.0.0.1/healthz || exit 1
SITE_URL передаётся во время сборки, потому что Astro использует его для canonical URL и sitemap. Изменение этой переменной после запуска готового контейнера уже не изменит статические страницы.
Dockerfile.dev с hot reload
Для разработки nginx не нужен. Контейнер запускает Astro dev server и слушает 0.0.0.0, чтобы сервер был доступен с хоста.
FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ENV SITE_URL=http://localhost:4321 \
ASTRO_TELEMETRY_DISABLED=1
EXPOSE 4321
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "4321"]
Флаг --host 0.0.0.0 обязателен для Docker. Если оставить стандартный localhost внутри контейнера, порт будет опубликован, но браузер на хосте не сможет подключиться к Astro.
Docker Compose для разработки и preview
Bind mount передаёт изменения исходников контейнеру, а отдельный volume не даёт хостовой папке перекрыть контейнерный node_modules. Каталог .astro изолирован в tmpfs, чтобы lock-файл dev-сервера с хоста не мешал повторному запуску контейнера.
services:
dev:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "4321:4321"
environment:
SITE_URL: http://localhost:4321
volumes:
- .:/app
- node_modules:/app/node_modules
tmpfs:
- /app/.astro
preview:
build:
context: .
dockerfile: Dockerfile
args:
SITE_URL: http://localhost:8081
ports:
- "8081:80"
volumes:
node_modules:
Запуск разработки:
docker compose up --build dev
После запуска сайт доступен на http://localhost:4321. Изменения в src/ видны без пересборки образа. Пересобрать dev-образ нужно после изменения package.json, package-lock.json или самого Dockerfile.dev.
Production preview запускается отдельно:
docker compose up --build preview
Он доступен на http://localhost:8081 и позволяет проверить тот же тип образа, который отправится на сервер.
Makefile с основными командами
Makefile не добавляет новой логики. Он хранит короткие имена для команд, которые легко забыть или набрать с ошибкой.
PORT ?= 4321
SITE_URL ?= http://localhost:$(PORT)
.PHONY: install run dev up preview check build down logs
install:
npm ci
run:
SITE_URL=$(SITE_URL) npm run dev -- --host 0.0.0.0 --port $(PORT)
dev:
PORT=$(PORT) SITE_URL=$(SITE_URL) docker compose up --build dev
up:
PORT=$(PORT) SITE_URL=$(SITE_URL) docker compose up --build -d dev
preview:
docker compose up --build preview
check:
npm run check
npm run build
build:
SITE_URL=$(SITE_URL) npm run build
logs:
docker compose logs --tail=100 -f dev
down:
docker compose down --remove-orphans
Повседневный цикл получается коротким:
make up
make logs
make check
make down
Как опубликовать новую статью
Создайте Markdown-файл в каталоге нужного языка. Например:
src/content/writing/ru/docker-healthcheck.md
Добавьте frontmatter и текст статьи:
---
locale: ru
translationKey: docker-healthcheck
routeSlug: docker-healthcheck
title: Как проверить состояние контейнера
description: Добавляем healthcheck и используем его в Docker Compose.
pubDate: 2026-09-01
dateLabel: 1 сентября 2026
readingTime: 5 мин
category: Docker
tags:
- Docker
- Operations
draft: true
---
Коротко объясните задачу и ожидаемый результат.
## Добавляем healthcheck
Опишите решение и добавьте проверяемый пример кода.
Пока draft: true, статья не появится в списке и production-сборке. Перед публикацией:
- закончите текст и замените
draftнаfalse; - запустите
make check; - откройте
/writing/docker-healthcheckлокально; - проверьте заголовок, описание, дату, листинги и мобильную версию;
- закоммитьте Markdown-файл и отправьте изменение в основную ветку.
Для английского перевода создайте файл в src/content/writing/en/. Используйте тот же translationKey, но переведите остальные поля. routeSlug может отличаться: он отвечает только за URL конкретного языка.
Что происходит после публикации
CI выполняет проверку Content Collections, собирает статические страницы и создаёт production-образ. После деплоя nginx начинает отдавать новую версию целиком. База данных, миграции и очистка кэша для публикации статьи не нужны.
Для небольшого инженерного блога этого достаточно: статья остаётся обычным файлом, локальная и production-сборки используют один код, а готовый образ можно проверить до отправки на сервер. Цена в том, что публикация это коммит и прогон CI: редактору без гита сюда не зайти, а данным, которые меняются чаще деплоя, здесь негде жить.