Astro на сервере: частые ошибки и решения
Astro хвалят за «островную» архитектуру: HTML собирается на сервере или во время билда, а JavaScript на клиент отдаётся только там, где он реально нужен — для интерактивных компонентов. На бумаге всё просто: npm run build и деплой. На практике же именно из-за этой гибридности — Astro умеет быть и чисто статическим генератором, и SSR-сервером одновременно — новички спотыкаются в одних и тех же местах. Разберём конкретные симптомы и что с ними делать на своём VPS.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Static vs server: с какого режима начинать разбор ошибки
Первое, что нужно понять перед любой диагностикой — в каком режиме собран проект. От этого зависит, где искать причину.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'static', // или 'server', или 'hybrid'
adapter: node({ mode: 'standalone' }), // нужен только для server/hybrid
});
output: 'static'— Astro генерирует чистый HTML/CSS/JS в папкуdist/. Отдавать такой сайт может любой веб-сервер: nginx, Apache, даже статический хостинг. Адаптер (node,vercelи т.д.) здесь не нужен и будет проигнорирован.output: 'server'— весь сайт рендерится на каждый запрос. Обязательно нужен адаптер (@astrojs/nodeдля своего VPS), и послеnpm run buildвы получаете не набор HTML-файлов, а Node.js-приложение вdist/server/entry.mjs, которое нужно запускать процессом.output: 'hybrid'(в новых версиях — комбинацияoutput: 'server'сexport const prerender = trueна отдельных страницах) — часть страниц статична, часть рендерится динамически.
Половина «непонятных» ошибок на сервере — это попытка обслуживать server-сборку как статику (получаете пустую страницу или голый JS-бандл без HTML) или наоборот — искать несуществующий Node-процесс для static-сборки. Проверьте output в конфиге и структуру dist/ перед тем, как копать глубже.
Сборка падает или зависает: out of memory на VPS
Симптом: npm run build обрывается без внятной ошибки, в логах Killed, либо процесс просто зависает на 100% CPU и никогда не завершается. Это почти всегда нехватка памяти — сборка Vite (на котором работает Astro) под капотом требует заметно больше RAM, чем сам итоговый сайт.
На VPS с 1 ГБ RAM сборка среднего проекта с несколькими интеграциями (React, MDX, Tailwind) вполне может упереться в лимит. Проверить это просто:
free -h
dmesg | grep -i "killed process"
Если во втором выводе видите Killed process рядом с node, — вопрос закрыт, это OOM. Решения по приоритету:
- Добавить swap, если его нет — самый быстрый способ пережить пиковую нагрузку при сборке, не апгрейдя тариф:
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
Подробно про выбор размера и постоянную настройку — в отдельной статье про swap-файл на сервере.
- Собирать не на проде, а в CI или локально, заливая на сервер уже готовый
dist/. Это вообще снимает вопрос памяти для сборки на боевом VPS — там остаётся только раздача статики или запуск Node-процесса. - Ограничить память Node явно, если хотите быстрее увидеть честную ошибку вместо зависания:
NODE_OPTIONS="--max-old-space-size=896" npm run build
- Если ничего не помогает и сборка стабильно требует больше 2 ГБ — это повод пересмотреть план VPS, а не бороться с симптомом бесконечно.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверSSR на Node-адаптере: 502 и 504 после деплоя
Для output: 'server' Astro собирает не сайт, а приложение, которое нужно запустить и держать живым процессом. Забытый этот шаг — самая частая причина 502 Bad Gateway сразу после деплоя.
Проверка, что процесс вообще жив:
node dist/server/entry.mjs
# сервер поднимется на порту из PORT (по умолчанию 4321 или 3000 в зависимости от версии адаптера)
Если руками сервер стартует, а через nginx выдаёт 502 — значит, наружу приложение не слушает то, что ожидает прокси, либо процесс просто упал и не перезапустился. Для боевого запуска Node-процесс нужно держать через systemd или PM2, а не в интерактивной сессии:
# /etc/systemd/system/astro-app.service
[Unit]
Description=Astro SSR app
After=network.target
[Service]
Type=simple
WorkingDirectory=/var/www/astro-site
Environment=PORT=4321
Environment=HOST=127.0.0.1
Environment=NODE_ENV=production
ExecStart=/usr/bin/node dist/server/entry.mjs
Restart=on-failure
RestartSec=3
User=www-data
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now astro-app
systemctl status astro-app
journalctl -u astro-app -f
Если процесс живой, а nginx всё равно отдаёт 502 или 504 — проверьте, что proxy_pass указывает на тот же порт, что и PORT в юните, и что таймауты прокси достаточны для медленных SSR-запросов (например, если страница дожидается ответа от внешнего API). Разбор конкретных причин 502 и 504 — в статьях ошибка 502 Bad Gateway в nginx и nginx отдаёт 504 Gateway Timeout.
Nginx отдаёт 404 на assets и маршрутах после билда
Для статической сборки типичный конфиг nginx выглядит так:
server {
listen 80;
server_name example.com;
root /var/www/astro-site/dist;
index index.html;
location / {
try_files $uri $uri/ $uri/index.html =404;
}
location /_astro/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Частые причины 404, если сайт «в целом работает», но отдельные страницы или ассеты не находятся:
- Забыт
try_files— Astro по умолчанию генерируетabout/index.htmlдля маршрута/about, а неabout.html. Без$uri/index.htmlвtry_filesnginx честно вернёт 404 на чистые пути без слеша. baseв конфиге не совпадает с реальным путём — если сайт живёт не в корне домена, а в подпапке (base: '/blog'), это нужно явно указать вastro.config.mjs, иначе все внутренние ссылки и пути к ассетам будут ломаться.- Кеш браузера или CDN держит старую версию
_astro/*.jsс хешем из предыдущей сборки, а HTML уже новый и ссылается на несуществующий файл. Решается либо полной заменойdist/, а не наложением поверх, либо очисткой кеша CDN после деплоя. - SSR-режим и 404 на API-роутах (
src/pages/api/*.ts) — проверьте, что метод HTTP в запросе совпадает с экспортируемой функцией (GET,POSTи т.д.), Astro по умолчанию не проксирует все методы на один обработчик.
Если сайт живёт на VPS вместе с другими проектами и путаница возникает из-за виртуальных хостов — см. настройку нескольких сайтов на одном VPS.
PUBLIC_-переменные и путаница окружений
Astro жёстко разделяет переменные окружения на клиентские и серверные, и это вторая по частоте причина «на локали работало, на проде — нет»:
- Переменная, использованная в клиентском коде (в компоненте с
client:*директивой), должна начинаться сPUBLIC_— иначе на этапе сборки Vite её просто не подставит, и в браузере вы получитеundefined. - Переменные без префикса доступны только в серверном коде — на страницах с SSR, в
.astro-файлах на этапе рендера, в API-роутах. В клиентский бандл они никогда не попадут (это осознанная защита от утечки секретов), но и в браузере их не будет. - Файл
.envчитается Astro (через Vite) на этапе сборки, а не в рантайме контейнера. Это значит: если вы собираетеdist/в одном окружении, а поднимаете на сервере через systemd с другим наборомEnvironment=— статическиеPUBLIC_-переменные, которые уже «зашиты» в JS-бандл на этапе билда, не изменятся от того, что вы поменяли.envна проде. Пересборка обязательна.
# .env (для локальной разработки/сборки)
PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgres://user:pass@localhost:5432/db
Для SSR-приложений на сервере, где нужны свежие серверные переменные без пересборки при каждом изменении, их проще прокидывать через systemd-юнит (Environment= или EnvironmentFile=/etc/astro-app.env), а не через .env-файл в рабочей директории.
sharp и оптимизация изображений падает при сборке
Компонент <Image /> и встроенная оптимизация изображений в Astro используют библиотеку sharp, у которой есть нативные бинарные зависимости под конкретную архитектуру и ОС. Типичная ошибка на сервере:
Error: Could not load the "sharp" module using the linux-x64 runtime
Возникает это почти всегда по одной причине: node_modules был скопирован с локальной машины (macOS/Windows) на Linux-сервер вместо переустановки. Бинарники sharp под разные платформы несовместимы.
Решение:
rm -rf node_modules package-lock.json
npm install
Устанавливать зависимости нужно на целевой системе — либо прямо на VPS, либо в CI с тем же образом ОС, что и на проде (Docker-сборка снимает это несоответствие полностью). Если сервер собирает через Docker, убедитесь, что базовый образ — node:20-bookworm или аналог с поддержкой нужных нативных модулей, а не -alpine без дополнительных пакетов: у Alpine другая libc (musl вместо glibc), и sharp там иногда требует отдельной сборки или падает вовсе.
Если оптимизация изображений на сервере с малым объёмом RAM (1 ГБ и меньше) стабильно роняет процесс сборки даже после переустановки зависимостей — это снова вопрос ресурсов при обработке крупных исходных файлов, и добавленный swap из второго раздела часто снимает проблему.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Нужен ли Node.js на сервере, если сайт собран как output: 'static'?
Нет, для раздачи готового dist/ достаточно nginx или любого веб-сервера. Node нужен только на этапе сборки (её можно делать и не на проде) и обязателен постоянно, только если у вас output: 'server'.
Можно ли автоматизировать деплой Astro-сайта при пуше в git?
Да, стандартная схема — git hook или CI, который тянет изменения, ставит зависимости, собирает dist/ и для SSR перезапускает systemd-юнит (systemctl restart astro-app). Общий подход описан в статье про автодеплой из Git на VPS.
Почему после деплоя видна старая версия сайта, хотя файлы обновились?
Чаще всего это кеш браузера или CDN на статичных ассетах с длинным Cache-Control. Хешированные имена файлов в _astro/ от этого не страдают, а вот сам index.html стоит отдавать с коротким или нулевым кешем.
Чем hybrid/частичный prerender отличается от полного SSR по нагрузке на сервер?
При prerender = true на конкретных страницах Astro рендерит их один раз при сборке, и дальше они отдаются как статика — без обращения к Node-процессу на каждый запрос. Это снижает нагрузку там, где контент не меняется постоянно, и есть смысл размечать так все страницы, которым не нужен рендер на лету.
Обязательно ли использовать именно @astrojs/node, если сервер свой?
Нет, это самый прямой вариант для VPS, но подходят и другие адаптеры со standalone-режимом; главное — чтобы итоговый процесс можно было запустить как обычный Node-сервис за reverse proxy.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →