Деплой Astro SSR на Selectel: пошаговый гайд
Astro в режиме SSR — мой default для контентных сайтов с динамическими кусками. Selectel — стабильный российский провайдер, на котором у меня крутится несколько production-сайтов. Расскажу, как я разворачиваю Astro SSR на Selectel VPS: от базовой настройки сервера до nginx с TLS и автодеплоя.
Контекст: Astro 5, Node-адаптер, Selectel Cloud Server (минимальный 2 vCPU / 4 GB RAM), Ubuntu 24.04. Те же шаги работают на других хостерах с минимальными изменениями.
Создание сервера в Selectel
В панели Selectel заводим Cloud Server. Что выбираю:
- Регион: ru-1 (Москва). Для российских пользователей — низкая латентность.
- Образ: Ubuntu 24.04 LTS.
- Конфигурация: 2 vCPU / 4 GB / 30 GB SSD — для контентного сайта с трафиком до 10000 посетителей в сутки этого хватает с запасом.
- SSH-ключи: загружаю свой публичный ключ. Парольный вход не использую вообще.
После создания сервера получаю IP. Привязываю в настройках регистратора домена A-запись на этот IP. Пока DNS прогревается, иду настраивать сервер.
Базовая настройка сервера
Подключаюсь по SSH под root и сразу делаю обвязку:
apt update && apt upgrade -y
# часовой пояс и точное время
timedatectl set-timezone Europe/Moscow
apt install -y chrony
systemctl enable --now chrony
# базовая безопасность
apt install -y ufw fail2ban
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
# обновления безопасности автоматически
apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgradesСоздаю пользователя для приложения:
useradd --create-home --shell /bin/bash deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keysОтключаю root-логин по SSH:
sed -i 's/^#?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshДальше работаю под пользователем deploy.
Установка Node и pnpm
Беру Node 20 из NodeSource — стабильно и без сюрпризов:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt install -y nodejs
node -v # должен показать v20.x
# pnpm
sudo npm install -g pnpm@9Подготовка проекта Astro
В проекте Astro подключаю Node-адаптер:
pnpm add @astrojs/nodeВ astro.config.mjs:
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone',
}),
site: 'https://example.ru',
});standalone вместо middleware — режим, в котором Astro собирает самостоятельный Node-сервер. На входе он слушает порт и обрабатывает запросы. Это удобнее, чем встраивать в Express.
В package.json:
{
"scripts": {
"build": "astro build",
"start": "node ./dist/server/entry.mjs"
}
}Структура на сервере
Раскладываю по знакомому шаблону:
sudo mkdir -p /opt/site/{releases,shared}
sudo chown -R deploy:deploy /opt/site
touch /opt/site/shared/.env
chmod 600 /opt/site/shared/.envВ .env кладу HOST=127.0.0.1, PORT=4321 и любые другие переменные приложения (DATABASE_URL, ключи API).
systemd-юнит
/etc/systemd/system/site.service:
[Unit]
Description=Astro SSR site
After=network.target
[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/opt/site/current
ExecStart=/usr/bin/node /opt/site/current/dist/server/entry.mjs
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=3
Environment=NODE_ENV=production
EnvironmentFile=/opt/site/shared/.env
StandardOutput=journal
StandardError=journal
SyslogIdentifier=site
[Install]
WantedBy=multi-user.targetАктивирую:
sudo systemctl daemon-reload
sudo systemctl enable --now site.service
sudo systemctl status site.serviceAstro слушает на 127.0.0.1:4321. Снаружи он недоступен — только через nginx.
nginx и сертификат
Установка nginx и certbot:
sudo apt install -y nginx certbot python3-certbot-nginxКонфиг /etc/nginx/sites-available/site.conf:
server {
listen 80;
listen [::]:80;
server_name example.ru www.example.ru;
location /.well-known/acme-challenge/ { root /var/www/certbot; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.ru www.example.ru;
ssl_certificate /etc/letsencrypt/live/example.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.ru/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_session_cache shared:SSL:10m;
ssl_stapling on;
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Content-Type-Options nosniff always;
server_tokens off;
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
location /_astro/ {
alias /opt/site/current/dist/client/_astro/;
expires 1y;
access_log off;
add_header Cache-Control "public, immutable";
}
location / {
proxy_pass http://127.0.0.1:4321;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Раздача _astro/ напрямую через nginx — обязательная оптимизация. Astro кладёт туда хеш-имена файлов с immutable-политикой, и обращений к ним очень много на каждой странице. Через nginx это в разы быстрее.
sudo ln -s /etc/nginx/sites-available/site.conf /etc/nginx/sites-enabled/
sudo mkdir -p /var/www/certbot
sudo nginx -t
sudo systemctl reload nginxПолучаю сертификат:
sudo certbot certonly --webroot -w /var/www/certbot \
-d example.ru -d www.example.ru \
--email admin@example.ru --agree-tos --no-eff-email --key-type ecdsaПосле получения — раскомментировать ssl-блок (если до этого там были заглушки) и сделать reload.
Деплой-скрипт
Простейший вариант на bash, запускаемый из CI:
#!/bin/bash
set -e
COMMIT=$(git rev-parse --short HEAD)
DATE=$(date +%F-%H%M)
RELEASE="${DATE}-${COMMIT}"
echo "Building locally..."
pnpm install --frozen-lockfile
pnpm build
echo "Uploading to ${RELEASE}..."
rsync -az --delete \
--exclude=node_modules \
--exclude=.git \
--exclude=.env \
./dist ./public ./package.json ./pnpm-lock.yaml \
deploy@example.ru:/opt/site/releases/${RELEASE}/
echo "Activating release..."
ssh deploy@example.ru bash <<EOF
set -e
cd /opt/site/releases/${RELEASE}
pnpm install --frozen-lockfile --prod
ln -sfn /opt/site/releases/${RELEASE} /opt/site/current_new
mv -Tf /opt/site/current_new /opt/site/current
sudo systemctl restart site.service
systemctl is-active --quiet site.service && echo "OK" || (echo "FAIL"; exit 1)
EOF
echo "Deployed ${RELEASE}"Команду sudo systemctl restart site.service делаю через sudoers.d: добавил deploy-пользователю право без пароля только на конкретный сервис.
# /etc/sudoers.d/deploy
deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart site.service, /bin/systemctl reload nginxБэкапы
На SSR-сайте чаще всего файлы статичные (репозиторий — источник правды), а данные — в БД. Бэкаплю:
- База данных — отдельный
pg_dumpв Selectel Object Storage. - Загруженные пользователями файлы (если есть) — синхронизация в Object Storage.
- Конфиги в
/etc/nginxи/etc/systemd/system— раз в неделю в git-репозиторий с серверными конфигами.
Selectel Object Storage S3-совместим, заводится через панель за пять минут. Стоимость на маленьких бэкапах — копейки в месяц.
Мониторинг
Минимум, что я ставлю:
- UptimeRobot или собственный health-check со стороны: пингует
/healthzили/раз в минуту, шлёт алерт при недоступности. - Netdata или Selectel Monitoring для системных метрик (CPU, RAM, диск, сеть). На минимальном уровне — бесплатно.
- Лог-агрегацию с
vector-ом, который читает journald и шлёт в общий хранилище.
Подводные камни
Astro и proxy headers
Если Astro обращается к request.url для построения абсолютных ссылок, без trust proxy у тебя ссылки будут http://, а не https://. В Astro адаптер honors X-Forwarded-Proto, но проверь, что nginx его передаёт.
Кэш статики
Если у тебя CDN перед nginx (Cloudflare, Selectel CDN), убедись, что immutable-файлы реально кэшируются, а HTML — нет. У меня было: cache-control правильный, но Cloudflare всё равно кэшировал HTML на сутки. Лечится Cache Rules в CDN.
Astro version updates
Astro меняется быстро, бывают breaking changes между мажорными. Перед апгрейдом прогоняй полный билд и smoke-тест локально, а потом на staging. У меня в одном проекте мажорный апгрейд занял часа два с подгонкой адаптера.
Размер node_modules
Astro и плагины тянут много зависимостей. На VPS с 30 GB места это не проблема, но 2–3 параллельных релиза в releases/ уже выходят на 1.5 ГБ. Я держу не больше пяти последних, остальные удаляю в скрипте.
Что я в итоге получаю
- Astro SSR на собственном VPS с TLS.
- Деплой одной командой из CI или с локальной машины.
- nginx раздаёт статику, проксирует динамику.
- Бэкапы в Object Storage.
- Мониторинг и алерты.
Сетап стандартный, но именно от его правильной комплектации зависит, будет ли сайт стабильно работать. Selectel — спокойный хостер, который не подбрасывает сюрпризов: тарифы предсказуемые, поддержка отвечает по делу, инфраструктура работает. У меня туда переехал один сайт с зарубежного хостера в 2024-м, претензий с тех пор нет.