Astro Deployment
Deploying Astro sites to Vercel, Netlify, Cloudflare Pages, and other platforms
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 linesDeployment — 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. ThePUBLIC_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
siteconfig and relying on relative URLs. Without an explicitsitevalue, sitemaps, RSS feeds, and canonical tags generate incorrect or missing URLs. Set it once inastro.config.mjsand referenceAstro.sitethroughout. -
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_modulesand 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
siteproperty inastro.config.mjsto your production URL. Astro uses this for sitemap generation, canonical URLs, and RSS feeds. - Use
npm run previewto test the production build locally before deploying. It catches issues thatnpm run devdoes 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 withPUBLIC_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
siteconfig: Without asitevalue, features like sitemap, RSS, andAstro.urlin 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(useglobalThis.cryptoinstead), 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
basefor subdirectory deployments: If your site lives atexample.com/docs/, omittingbase: '/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
Related Skills
Astro Basics
Astro fundamentals including project structure, components, islands architecture, and templating syntax
Astro Content Collections
Content collections in Astro for managing Markdown, MDX, JSON, and YAML content with type-safe schemas
Astro Integrations
Using React, Vue, Svelte, and other UI framework islands within Astro pages
Astro Middleware
Middleware patterns in Astro for authentication, request modification, response headers, and shared context
Astro Routing
File-based and dynamic routing in Astro including static paths, rest parameters, and route priority
Astro SSR
Server-side rendering in Astro with adapters for Node, Vercel, Netlify, Cloudflare, and Deno