Перейти к содержимому

27. SvelteKit: Деплой

Привет! 👋 Написать приложение — это полдела. Нужно ещё его задеплоить! SvelteKit с его системой адаптеров делает это удивительно гибким. Один и тот же код можно задеплоить на Vercel, Netlify, Cloudflare Workers, обычный VPS или даже как статический сайт. Выбирай что хочешь!


Адаптер — это пакет npm, который трансформирует собранное SvelteKit приложение в формат, понятный конкретной платформе.

npm run build
[SvelteKit компиляция]
[Адаптер обрабатывает вывод]
Готовый артефакт для платформы

adapter-auto — это дефолтный адаптер при создании проекта. Он автоматически определяет платформу по переменным окружения:

// svelte.config.js с adapter-auto
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
},
};
export default config;

Что он делает:

  • VERCEL env → использует adapter-vercel
  • NETLIFY env → использует adapter-netlify
  • CF_PAGES env → использует adapter-cloudflare-pages
  • Ничего нет → падает с ошибкой (нужен явный адаптер)

Окно терминала
npm install -D @sveltejs/adapter-node
svelte.config.js
import adapter from '@sveltejs/adapter-node';
const config = {
kit: {
adapter: adapter({
out: 'build', // Папка вывода
precompress: false, // Предсжатие gzip/brotli
envPrefix: '', // Префикс для env переменных
}),
},
};

Dockerfile для Node.js:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production образ (минимальный):
FROM node:20-alpine AS runner
WORKDIR /app
# Только нужные файлы:
COPY --from=builder /app/build ./build
COPY --from=builder /app/package*.json ./
# Устанавливаем только prod зависимости:
RUN npm ci --only=production
# Открываем порт:
EXPOSE 3000
# Запускаем:
CMD ["node", "build/index.js"]

docker-compose.yml:

version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- PORT=3000
- ORIGIN=https://myapp.com
- DATABASE_URL=postgresql://db:5432/myapp
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:

PM2 для управления процессом:

Окно терминала
# Глобальная установка PM2:
npm install -g pm2
# ecosystem.config.js:
module.exports = {
apps: [{
name: 'sveltekit-app',
script: './build/index.js',
instances: 'max', // По одному на каждое ядро CPU
exec_mode: 'cluster', // Режим кластера
env_production: {
NODE_ENV: 'production',
PORT: 3000,
ORIGIN: 'https://myapp.com',
},
}],
};
# Запуск:
pm2 start ecosystem.config.js --env production
# Автостарт после перезагрузки:
pm2 startup
pm2 save

Окно терминала
npm install -D @sveltejs/adapter-vercel
svelte.config.js
import adapter from '@sveltejs/adapter-vercel';
const config = {
kit: {
adapter: adapter({
runtime: 'nodejs20.x', // Node.js версия
regions: ['fra1'], // Регион (Frankfurt)
// Edge Functions вместо Node.js:
// runtime: 'edge',
}),
},
};

Per-route конфигурация на Vercel:

// src/routes/api/fast/+server.ts — Edge Function
export const config = {
runtime: 'edge', // Этот route на Edge
regions: ['iad1', 'fra1'], // Регионы
};
// src/routes/heavy/+page.server.ts — обычный serverless
export const config = {
runtime: 'nodejs20.x',
maxDuration: 60, // Максимум 60 секунд
};

ISR (Incremental Static Regeneration) на Vercel:

src/routes/blog/+page.server.ts
export const config = {
isr: {
expiration: 60, // Ревалидировать через 60 секунд
// bypassToken: process.env.BYPASS_TOKEN, // Для on-demand ISR
},
};

Окно терминала
npm install -D @sveltejs/adapter-netlify
svelte.config.js
import adapter from '@sveltejs/adapter-netlify';
const config = {
kit: {
adapter: adapter({
edge: false, // Использовать Netlify Functions (не Edge)
split: false, // Один бандл для всех функций
}),
},
};

netlify.toml:

[build]
command = "npm run build"
publish = "build"
[build.environment]
NODE_VERSION = "20"
# Редиректы:
[[redirects]]
from = "/old-path"
to = "/new-path"
status = 301
# Заголовки безопасности:
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"

Окно терминала
npm install -D @sveltejs/adapter-cloudflare
// svelte.config.js для Cloudflare
import adapter from '@sveltejs/adapter-cloudflare';
const config = {
kit: {
adapter: adapter({
routes: {
include: ['/*'],
exclude: ['<all>'], // Статические файлы напрямую
},
}),
},
};

Доступ к Cloudflare Workers API:

// src/app.d.ts — типы для Cloudflare platform
declare global {
namespace App {
interface Platform {
env: {
// KV Storage:
CACHE: KVNamespace;
// D1 Database:
DB: D1Database;
// R2 Storage:
ASSETS: R2Bucket;
// Дополнительные Workers Services:
AI: Ai;
};
context: ExecutionContext;
caches: CacheStorage & { default: Cache };
}
}
}
// src/routes/api/cached/+server.ts — использование KV Cache
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
export const GET: RequestHandler = async ({ platform, params }) => {
const { env } = platform!;
const cacheKey = `product:${params.id}`;
// Проверяем KV кэш:
const cached = await env.CACHE.get(cacheKey);
if (cached) return json(JSON.parse(cached));
// Запрашиваем из D1:
const result = await env.DB
.prepare('SELECT * FROM products WHERE id = ?')
.bind(params.id)
.first();
// Сохраняем в KV на 5 минут:
await env.CACHE.put(cacheKey, JSON.stringify(result), {
expirationTtl: 300,
});
return json(result);
};

wrangler.toml для конфигурации:

name = "my-sveltekit-app"
main = "./build/index.js"
compatibility_date = "2024-01-01"
pages_build_output_dir = ".svelte-kit/cloudflare"
[[kv_namespaces]]
binding = "CACHE"
id = "xxxxxxxxxx"
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxxxxxxxx"

Окно терминала
npm install -D @sveltejs/adapter-static
svelte.config.js
import adapter from '@sveltejs/adapter-static';
const config = {
kit: {
adapter: adapter({
pages: 'build', // Куда кладём HTML
assets: 'build', // Куда кладём статику
fallback: '404.html', // SPA fallback (или 'index.html' для SPA)
precompress: true, // Gzip + Brotli предсжатие
strict: true, // Ошибка если страница не предрендерена
}),
prerender: {
entries: ['*'], // Предрендеривать все страницы
},
},
};

Настройка предрендеринга:

src/routes/blog/[slug]/+page.ts
import type { EntryGenerator, PageLoad } from './$types';
// Генерируем список всех слагов для предрендеринга:
export const entries: EntryGenerator = async () => {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
};
export const prerender = true;
export const load: PageLoad = async ({ params }) => {
const post = await getPost(params.slug);
return { post };
};
src/routes/dashboard/+page.ts
// Отключить предрендеринг для динамических страниц:
export const prerender = false;
export const ssr = false; // SPA режим для динамических страниц

Модуль Тип Доступен где
──────────────────────────────────────────────────────────────────
$env/static/public Билд-тайм Сервер + Клиент (PUBLIC_ prefix)
$env/static/private Билд-тайм Только сервер
$env/dynamic/public Runtime Сервер + Клиент (PUBLIC_ prefix)
$env/dynamic/private Runtime Только сервер
// Примеры использования:
import { PUBLIC_API_URL, PUBLIC_STRIPE_KEY } from '$env/static/public';
import { DATABASE_URL, SECRET_KEY, STRIPE_SECRET } from '$env/static/private';
import { env as dynamicPrivate } from '$env/dynamic/private';
// static — вшивается в бандл при сборке (оптимальнее):
const apiUrl = PUBLIC_API_URL; // 'https://api.myapp.com'
// dynamic — читается при каждом запросе (нужно для runtime конфигурации):
const dbUrl = dynamicPrivate.DATABASE_URL;
// ⚠️ Нельзя использовать $env/static/private в +page.svelte или +page.ts!
// Только в +page.server.ts, +layout.server.ts, +server.ts, hooks.server.ts

Типизация переменных окружения:

src/app.d.ts
declare global {
namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
SECRET_KEY: string;
PUBLIC_API_URL: string;
}
}
}

Окно терминала
# Обязательно для Node.js деплоя!
# ORIGIN указывает откуда приходят запросы — нужен для CSRF защиты
# В production:
ORIGIN=https://myapp.com
# При деплое через Docker/PM2 обязательно задать:
PORT=3000
ORIGIN=https://myapp.com
// Если ORIGIN не задан — SvelteKit падает с ошибкой для Form Actions
// Это защищает от CSRF атак

// svelte.config.js — глобальные настройки предрендеринга
const config = {
kit: {
prerender: {
// Что делать при HTTP ошибках во время предрендеринга:
handleHttpError: ({ path, referrer, message }) => {
if (path.includes('/api/')) return; // Игнорируем ошибки API
throw new Error(message);
},
// Что делать при отсутствии якоря в ссылке:
handleMissingId: 'warn', // 'ignore' | 'warn' | 'error'
// Точки входа для предрендеринга:
entries: ['*'], // Все страницы
// или ['/', '/about', '/contact']
// Параллельность предрендеринга:
concurrency: 4,
// Предотвратить краулинг:
crawl: true,
},
},
};

.github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npm run check # TypeScript проверка
- run: npm run test # Тесты
- run: npm run lint # Линтер
build-and-deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npm run build
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_API_URL: ${{ vars.PUBLIC_API_URL }}
- name: Deploy to Server
run: |
rsync -avz --delete ./build user@server:/app/build
ssh user@server "pm2 reload sveltekit-app"