Skip to main content
Technology & EngineeringAstro417 lines

Astro Deployment

Deploying Astro sites to Vercel, Netlify, Cloudflare Pages, and other platforms

Quick Summary32 lines
You are an expert in deploying Astro websites to Vercel, Netlify, Cloudflare Pages, and other hosting platforms.

## Key Points

- Set the `site` property in `astro.config.mjs` to your production URL. Astro uses this for sitemap generation, canonical URLs, and RSS feeds.
- Use `npm run preview` to test the production build locally before deploying. It catches issues that `npm run dev` does not.
- Pin your Node.js version in your hosting config and CI/CD to avoid build inconsistencies. Astro requires Node 18.17.1 or later.
- Use platform-specific image optimization (Vercel Image Optimization, Cloudflare Images) when available, as they offload processing from the build step.
- Keep secrets out of `PUBLIC_` environment variables. Only variables prefixed with `PUBLIC_` are exposed to client-side code.
- For static sites, prefer platforms with built-in CDN (Vercel, Netlify, Cloudflare Pages) to get edge caching without extra configuration.
- **Missing `site` config**: Without a `site` value, features like sitemap, RSS, and `Astro.url` in static mode produce incorrect URLs.
- **Wrong adapter for the platform**: Deploying an SSR site without the correct adapter (or with no adapter) produces a static build that ignores server-rendered pages.
- **Forgetting `base` for subdirectory deployments**: If your site lives at `example.com/docs/`, omitting `base: '/docs'` breaks all asset and link paths.

## Quick Example

```bash
# Build the site
npm run build

# Output is in dist/
ls dist/
```

```bash
npx astro add vercel    # Vercel
npx astro add netlify   # Netlify
npx astro add cloudflare # Cloudflare Pages
npx astro add node      # Self-hosted Node.js
```
skilldb get astro-skills/Astro DeploymentFull skill: 417 lines
Paste into your CLAUDE.md or agent config

Deployment — Astro

You are an expert in deploying Astro websites to Vercel, Netlify, Cloudflare Pages, and other hosting platforms.

Overview

Astro supports deployment to virtually any hosting platform. Static sites can be deployed anywhere that serves HTML files. SSR sites need an adapter that matches the hosting platform's runtime. Astro's build output is optimized for each target through its adapter system.

Core Concepts

Static Deployment

For static sites (output: 'static'), Astro builds to dist/ by default. Upload this directory to any static host:

# Build the site
npm run build

# Output is in dist/
ls dist/

Adapter-Based Deployment

For SSR sites, install the adapter matching your platform:

npx astro add vercel    # Vercel
npx astro add netlify   # Netlify
npx astro add cloudflare # Cloudflare Pages
npx astro add node      # Self-hosted Node.js

Implementation Patterns

Vercel

Vercel auto-detects Astro projects. For full control:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel';

export default defineConfig({
  output: 'server',
  adapter: vercel({
    // Use Edge Functions for faster cold starts
    // edgeMiddleware: true,

    // Enable ISR (Incremental Static Regeneration)
    // isr: true,

    // Image optimization
    imageService: true,

    // Web Analytics
    webAnalytics: { enabled: true },
  }),
});

Vercel configuration for monorepos or custom settings:

// vercel.json
{
  "buildCommand": "npm run build",
  "outputDirectory": "dist",
  "framework": "astro",
  "headers": [
    {
      "source": "/fonts/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
      ]
    }
  ]
}

Environment variables in Vercel:

---
// Server-side (safe for secrets)
const apiKey = import.meta.env.API_KEY;

// Client-side (PUBLIC_ prefix required)
const analyticsId = import.meta.env.PUBLIC_ANALYTICS_ID;
---

Netlify

// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';

export default defineConfig({
  output: 'server',
  adapter: netlify({
    // Use Edge Functions instead of Netlify Functions
    // edgeMiddleware: true,

    // Image CDN
    imageCDN: true,
  }),
});

Netlify configuration:

# netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

[build.environment]
  NODE_VERSION = "20"

[[headers]]
  for = "/fonts/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

[[redirects]]
  from = "/old-page"
  to = "/new-page"
  status = 301

Cloudflare Pages

// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
  output: 'server',
  adapter: cloudflare({
    // Access Cloudflare bindings (KV, D1, R2, etc.)
    platformProxy: {
      enabled: true,
    },
  }),
});

Access Cloudflare bindings in pages and endpoints:

// src/pages/api/data.ts
import type { APIRoute } from 'astro';

export const GET: APIRoute = async ({ locals }) => {
  // Access KV, D1, R2 through the runtime
  const { env } = locals.runtime;

  const value = await env.MY_KV_NAMESPACE.get('key');

  // D1 database query
  const { results } = await env.DB.prepare(
    'SELECT * FROM users WHERE id = ?'
  ).bind(1).all();

  return new Response(JSON.stringify({ value, results }), {
    headers: { 'Content-Type': 'application/json' },
  });
};

Cloudflare Pages configuration:

# wrangler.toml (for bindings)
name = "my-astro-site"
compatibility_date = "2026-01-01"

[[kv_namespaces]]
binding = "MY_KV_NAMESPACE"
id = "abc123"

[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "def456"

Self-Hosted Node.js (Docker)

// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
});
# Dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./

ENV HOST=0.0.0.0
ENV PORT=3000
EXPOSE 3000

CMD ["node", "./dist/server/entry.mjs"]
# docker-compose.yml
version: '3.8'
services:
  web:
    build: .
    ports:
      - '3000:3000'
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
    restart: unless-stopped

GitHub Actions CI/CD

# .github/workflows/deploy.yml
name: Deploy Astro Site

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npm run build

      # For static sites deployed to GitHub Pages
      - uses: actions/upload-pages-artifact@v3
        with:
          path: dist/

  deploy:
    needs: build-and-deploy
    runs-on: ubuntu-latest
    permissions:
      pages: write
      id-token: write
    environment:
      name: github-pages
    steps:
      - uses: actions/deploy-pages@v4

Build Optimization

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  // Compress HTML output
  compressHTML: true,

  build: {
    // Inline small stylesheets
    inlineStylesheets: 'auto',
  },

  // Image optimization
  image: {
    service: {
      entrypoint: 'astro/assets/services/sharp',
    },
  },

  vite: {
    build: {
      // Increase chunk size warning limit
      chunkSizeWarningLimit: 1000,

      rollupOptions: {
        output: {
          // Manual chunk splitting
          manualChunks: {
            'react-vendor': ['react', 'react-dom'],
          },
        },
      },
    },
  },
});

Environment Variables

# .env (local development)
API_KEY=secret_key_here
PUBLIC_SITE_URL=http://localhost:4321

# .env.production (production build)
API_KEY=prod_secret_key
PUBLIC_SITE_URL=https://example.com
// src/env.d.ts — type your environment variables
interface ImportMetaEnv {
  readonly API_KEY: string;
  readonly PUBLIC_SITE_URL: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

Preview Before Deploy

# Build and preview locally with the exact output
npm run build
npm run preview

# For SSR sites with Node adapter, run the server directly
node dist/server/entry.mjs

Site Configuration for Deployment

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  // Required for correct asset paths
  site: 'https://example.com',

  // If deploying to a subdirectory
  base: '/my-app',

  // Trailing slash behavior
  trailingSlash: 'never', // 'always', 'never', or 'ignore'
});

Core Philosophy

Astro's deployment model reflects its content-first nature: static output is the default, and server rendering is an explicit opt-in. This means most Astro sites can deploy to any CDN or static host with zero configuration, giving you maximum portability and minimal operational complexity. The adapter system exists as a bridge for the minority of pages that truly need server-side rendering.

The build system treats deployment as a predictable, reproducible process. The output in dist/ is a self-contained artifact that does not depend on the source tree. Environment variables are resolved at build time for static pages, making the deployed site hermetic. When you combine this with content-hash-based asset fingerprinting, you get aggressive caching for free and confident cache invalidation on every deploy.

Choosing a deployment target is a permanent architectural decision, not just an infrastructure concern. The adapter you select determines which server APIs are available, which Node.js built-ins work, and what performance characteristics your SSR pages exhibit. Astro's design encourages you to make this decision early and configure it explicitly rather than discovering platform constraints after the code is written.

Anti-Patterns

  • Deploying SSR sites without testing the specific adapter locally. Each adapter (Vercel, Cloudflare, Node) has different runtime constraints. Code that works with the Node adapter may fail on Cloudflare Workers due to missing built-ins. Always preview with the production adapter before shipping.

  • Using PUBLIC_ environment variables for secrets. The PUBLIC_ prefix exposes values to client-side JavaScript bundles. API keys, tokens, and passwords must use non-prefixed variables that are only accessible in server-side code.

  • Omitting the site config and relying on relative URLs. Without an explicit site value, sitemaps, RSS feeds, and canonical tags generate incorrect or missing URLs. Set it once in astro.config.mjs and reference Astro.site throughout.

  • Bundling thousands of images for build-time optimization. Astro's image pipeline is powerful but runs at build time. Massive image libraries cause slow builds and can exceed platform time limits. Use external image CDNs for large media catalogs.

  • Ignoring platform-specific build caching. Rebuilding node_modules and all assets from scratch on every deploy is wasteful. Configure your CI/CD pipeline and hosting platform to cache dependencies and incremental build outputs.

Best Practices

  • Set the site property in astro.config.mjs to your production URL. Astro uses this for sitemap generation, canonical URLs, and RSS feeds.
  • Use npm run preview to test the production build locally before deploying. It catches issues that npm run dev does not.
  • Pin your Node.js version in your hosting config and CI/CD to avoid build inconsistencies. Astro requires Node 18.17.1 or later.
  • Use platform-specific image optimization (Vercel Image Optimization, Cloudflare Images) when available, as they offload processing from the build step.
  • Keep secrets out of PUBLIC_ environment variables. Only variables prefixed with PUBLIC_ are exposed to client-side code.
  • For static sites, prefer platforms with built-in CDN (Vercel, Netlify, Cloudflare Pages) to get edge caching without extra configuration.

Common Pitfalls

  • Missing site config: Without a site value, features like sitemap, RSS, and Astro.url in static mode produce incorrect URLs.
  • Wrong adapter for the platform: Deploying an SSR site without the correct adapter (or with no adapter) produces a static build that ignores server-rendered pages.
  • Node.js APIs on edge runtimes: Cloudflare Workers and Vercel Edge Functions do not support all Node.js built-in modules. Avoid fs, path, crypto (use globalThis.crypto instead), and other Node-specific APIs in edge-deployed code.
  • Large build output: Astro's image optimization runs at build time by default. Thousands of images can make builds very slow and hit platform build-time limits. Use external image services for large media libraries.
  • Forgetting base for subdirectory deployments: If your site lives at example.com/docs/, omitting base: '/docs' breaks all asset and link paths.
  • Environment variables not available at build time: Some platforms inject environment variables only at runtime. For static builds, all needed variables must be available during npm run build. Check your platform's documentation on build-time vs. runtime env vars.

Install this skill directly: skilldb add astro-skills

Get CLI access →