lenec ru

← все посты

Деплой Astro SSR на Selectel: пошаговый гайд

15K

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.service

Astro слушает на 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-м, претензий с тех пор нет.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.