Deployment & CI/CD
Deploying a SvelteKit application involves building the project, choosing the right adapter, and setting up a pipeline that tests, builds, and deploys on every push. The build step compiles your Svelte components into optimized JavaScript and CSS, the adapter transforms the output for your hosting platform, and CI/CD automates the entire process so deployments are repeatable and safe.
Building the Application
SvelteKit uses Vite as its build tool. The build step compiles everything:
npx vite build
This runs the Svelte compiler on your components, bundles client-side JavaScript, generates server-side code, prerenders static pages, and runs the adapter. The output depends on which adapter you configured.
Preview the production build locally before deploying:
npx vite preview
This starts a local server that serves the built output exactly as it would run in production. It catches issues that vite dev does not surface: missing environment variables, SSR-only code paths, and adapter-specific behavior.
Deploying to Cloudflare Pages
Cloudflare Pages is a strong default for SvelteKit applications. It handles static assets via a global CDN and runs server-side code on Cloudflare Workers at the edge.
npm install -D @sveltejs/adapter-cloudflare
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter()
}
};
export default config;
Connect your GitHub repository to Cloudflare Pages through the Cloudflare dashboard. Set the build command and output directory:
Build command: npx vite build
Build output directory: .svelte-kit/cloudflare
Every push to the main branch triggers a production deployment. Pull requests get preview deployments with unique URLs for testing before merge.
For manual deployments using Wrangler:
npx vite build
npx wrangler pages deploy .svelte-kit/cloudflare --project-name my-app
GitHub Actions for CI
A complete CI pipeline that tests, builds, and deploys your SvelteKit app:
# .github/workflows/ci.yml
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Type check
run: npx svelte-kit sync && npx tsc --noEmit
- name: Lint
run: npx eslint .
- name: Unit tests
run: npx vitest run
- name: Install Playwright
run: npx playwright install --with-deps chromium
- name: E2E tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results
path: |
playwright-report/
test-results/
build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Build
run: npx vite build
env:
PUBLIC_APP_URL: ${{ vars.PUBLIC_APP_URL }}
- uses: actions/upload-artifact@v4
with:
name: build-output
path: .svelte-kit/cloudflare/
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: build-output
path: .svelte-kit/cloudflare/
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy .svelte-kit/cloudflare --project-name my-app
The pipeline has three stages: test (lint, type check, unit tests, E2E tests), build (compile the application), and deploy (push to Cloudflare Pages). The deploy step only runs on pushes to main, not on pull requests.
Environment Variables in Production
SvelteKit distinguishes between build-time and runtime variables:
// Build-time: baked into the output during vite build
// Set these in GitHub Actions via vars or env
import { PUBLIC_APP_URL } from '$env/static/public';
import { API_KEY } from '$env/static/private';
// Runtime: read at request time
// Set these in your hosting platform's environment settings
import { DATABASE_URL } from '$env/dynamic/private';
import { PUBLIC_FEATURE_FLAG } from '$env/dynamic/public';
In GitHub Actions, build-time variables are set in the env block:
- name: Build
run: npx vite build
env:
PUBLIC_APP_URL: https://myapp.com
API_KEY: ${{ secrets.API_KEY }}
Runtime variables are configured in your hosting platform. For Cloudflare Pages, set them in the dashboard under Settings > Environment variables, or in wrangler.toml:
[vars]
PUBLIC_FEATURE_FLAG = "true"
# Secrets are set via the dashboard or wrangler secret put
Performance Monitoring
Track application performance with Lighthouse CI and Web Vitals.
Lighthouse CI in GitHub Actions:
# Add to your CI workflow
lighthouse:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: build-output
path: .svelte-kit/cloudflare/
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v12
with:
configPath: ./lighthouserc.json
uploadArtifacts: true
// lighthouserc.json
{
"ci": {
"collect": {
"startServerCommand": "npx vite preview",
"startServerReadyPattern": "localhost",
"url": [
"http://localhost:4173/",
"http://localhost:4173/blog",
"http://localhost:4173/about"
],
"numberOfRuns": 3
},
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"categories:accessibility": ["error", { "minScore": 0.9 }],
"categories:seo": ["error", { "minScore": 0.9 }]
}
}
}
}
Web Vitals tracking in your application:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { dev } from '$app/environment';
let { children } = $props();
onMount(async () => {
if (!dev) {
const { onCLS, onFID, onLCP, onFCP, onTTFB } = await import('web-vitals');
function sendMetric(metric: { name: string; value: number; id: string }) {
// Send to your analytics endpoint
navigator.sendBeacon('/api/vitals', JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
page: window.location.pathname
}));
}
onCLS(sendMetric);
onFID(sendMetric);
onLCP(sendMetric);
onFCP(sendMetric);
onTTFB(sendMetric);
}
});
</script>
{@render children()}
Deployment Checklist
Before deploying to production, verify these items:
// 1. Build succeeds with production environment variables
// npx vite build
// 2. Preview works locally
// npx vite preview
// 3. All tests pass
// npx vitest run && npx playwright test
// 4. Environment variables are set in the hosting platform
// - DATABASE_URL (runtime, private)
// - PUBLIC_APP_URL (build-time, public)
// - API_KEY (build-time, private)
// 5. Adapter is configured correctly
// svelte.config.js → adapter matches your platform
// 6. Prerendered routes build successfully
// Check build output for any prerender errors
// 7. Error pages exist
// src/routes/+error.svelte handles unexpected errors
Common Pitfalls
- Not testing the production build locally.
vite devuses different code paths than the production build. SSR behavior, environment variables, and adapter output can all differ. Always runvite build && vite previewbefore deploying. - Missing environment variables in CI. Build-time variables must be available during
vite buildin CI. Add them to your GitHub repository's settings under Secrets and Variables. - Deploying without running tests. Skip this once and you will ship a broken build. The CI pipeline should gate deployments on passing tests.
- Not setting up preview deployments. Without preview URLs for pull requests, you review code without seeing the result. Most hosting platforms support preview deployments natively.
- Ignoring Lighthouse regressions. Performance degrades gradually. Without automated checks, you only notice when users complain. Set minimum score thresholds and fail the build when they are not met.
- Hardcoding URLs. Using
http://localhost:5173in your code breaks in production. Use$app/environmentand environment variables for base URLs.
Key Takeaways
- Build with
vite build, preview withvite preview. Always test the production build before deploying. - Cloudflare Pages with adapter-cloudflare provides edge rendering, global CDN, and automatic preview deployments.
- A CI pipeline should run type checking, linting, unit tests, and E2E tests before allowing deployment.
- Build-time variables (
$env/static/*) are baked in during build. Runtime variables ($env/dynamic/*) are read per request. - Use Lighthouse CI to catch performance regressions automatically in pull requests.
- Track Web Vitals in production to monitor real-user performance over time.
- Gate production deployments on passing tests and successful builds. No exceptions.