3 min read
On this page

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 dev uses different code paths than the production build. SSR behavior, environment variables, and adapter output can all differ. Always run vite build && vite preview before deploying.
  • Missing environment variables in CI. Build-time variables must be available during vite build in 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:5173 in your code breaks in production. Use $app/environment and environment variables for base URLs.

Key Takeaways

  • Build with vite build, preview with vite 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.