Сайт сделан при помощи 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 --from=build /app/dist /usr/share/nginx/html

EXPOSE 80

HEALTHCHECK --interval=15s --timeout=3s --start-period=5s --retries=5 \
  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-сборке. Перед публикацией:

  1. закончите текст и замените draft на false;
  2. запустите make check;
  3. откройте /writing/docker-healthcheck локально;
  4. проверьте заголовок, описание, дату, листинги и мобильную версию;
  5. закоммитьте Markdown-файл и отправьте изменение в основную ветку.

Для английского перевода создайте файл в src/content/writing/en/. Используйте тот же translationKey, но переведите остальные поля. routeSlug может отличаться: он отвечает только за URL конкретного языка.

Что происходит после публикации

CI выполняет проверку Content Collections, собирает статические страницы и создаёт production-образ. После деплоя nginx начинает отдавать новую версию целиком. База данных, миграции и очистка кэша для публикации статьи не нужны.

Для небольшого инженерного блога этого достаточно: статья остаётся обычным файлом, локальная и production-сборки используют один код, а готовый образ можно проверить до отправки на сервер. Цена в том, что публикация это коммит и прогон CI: редактору без гита сюда не зайти, а данным, которые меняются чаще деплоя, здесь негде жить.