# Deployment Reference
Comprehensive guide to deploying Astro applications across platforms: Cloudflare, Vercel, Netlify, Node.js, and static hosting.
## Cloudflare Workers / Pages
### Setup
```bash
npx astro add cloudflare
```
```typescript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server', // or 'hybrid'
adapter: cloudflare({
imageService: 'cloudflare', // Use Cloudflare Image Resizing
platformProxy: {
enabled: true, // Enable local bindings in dev
},
}),
site: 'https://example.com',
});
```
### Wrangler Configuration
```toml
# wrangler.toml
name = "my-astro-site"
compatibility_date = "2024-11-01"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = "./dist"
# KV Namespace binding
[[kv_namespaces]]
binding = "CACHE"
id = "abc123"
# D1 Database binding
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "def456"
# R2 Bucket binding
[[r2_buckets]]
binding = "ASSETS"
bucket_name = "my-assets"
# Environment variables
[vars]
API_URL = "https://api.example.com"
# Secrets (set via wrangler secret put)
# SECRET_KEY - set via `wrangler secret put SECRET_KEY`
```
### Accessing Cloudflare Bindings
```typescript
// Type definitions for Cloudflare bindings
// src/env.d.ts
///
type Runtime = import('@astrojs/cloudflare').Runtime;
interface Env {
CACHE: KVNamespace;
DB: D1Database;
ASSETS: R2Bucket;
API_URL: string;
SECRET_KEY: string;
}
declare namespace App {
interface Locals extends Runtime {}
}
```
```astro
---
// src/pages/api/data.ts
import type { APIContext } from 'astro';
export async function GET({ locals }: APIContext) {
const { env } = locals.runtime;
// KV operations
const cached = await env.CACHE.get('my-key');
if (cached) {
return new Response(cached, {
headers: { 'Content-Type': 'application/json' },
});
}
// D1 database query
const { results } = await env.DB
.prepare('SELECT * FROM posts WHERE published = ?')
.bind(true)
.all();
// R2 object storage
const object = await env.ASSETS.get('images/hero.jpg');
// Cache the result
await env.CACHE.put('my-key', JSON.stringify(results), {
expirationTtl: 3600,
});
return new Response(JSON.stringify(results), {
headers: { 'Content-Type': 'application/json' },
});
}
```
### Cloudflare Middleware
```typescript
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async ({ locals, request, cookies }, next) => {
const { env } = locals.runtime;
// Auth check using KV
const session = cookies.get('session')?.value;
if (session) {
const user = await env.CACHE.get(`session:${session}`);
if (user) {
locals.user = JSON.parse(user);
}
}
// Rate limiting with KV
const ip = request.headers.get('CF-Connecting-IP') ?? 'unknown';
const rateKey = `rate:${ip}`;
const count = parseInt(await env.CACHE.get(rateKey) ?? '0');
if (count > 100) {
return new Response('Rate limited', { status: 429 });
}
await env.CACHE.put(rateKey, String(count + 1), { expirationTtl: 60 });
return next();
});
```
### Deployment
```bash
# Build and deploy to Cloudflare Pages
npm run build
npx wrangler pages deploy dist
# Or connect to Git for automatic deploys via Cloudflare Dashboard
# Settings > Build > Framework preset: Astro
```
## Vercel
### Setup
```bash
npx astro add vercel
```
```typescript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel';
export default defineConfig({
output: 'server', // or 'hybrid'
adapter: vercel({
imageService: true, // Use Vercel Image Optimization
isr: {
expiration: 60, // ISR: revalidate every 60 seconds
},
webAnalytics: {
enabled: true, // Enable Vercel Web Analytics
},
maxDuration: 30, // Serverless function timeout (seconds)
}),
});
```
### Serverless vs Edge
```typescript
// Default: serverless function
export default defineConfig({
output: 'server',
adapter: vercel(),
});
// Edge function (faster cold start, limited APIs)
export default defineConfig({
output: 'server',
adapter: vercel({
edgeMiddleware: true, // Run middleware at the edge
}),
});
```
### ISR (Incremental Static Regeneration)
```astro
---
// Per-page ISR configuration
// src/pages/blog/[slug].astro
export const prerender = false;
// Set ISR headers
Astro.response.headers.set(
'Cache-Control',
's-maxage=60, stale-while-revalidate=600'
);
---
```
### Vercel Environment Variables
```bash
# Set via Vercel CLI
vercel env add PRIVATE_KEY
vercel env add PUBLIC_API_URL
# Or via vercel.json
```
```json
// vercel.json
{
"framework": "astro",
"buildCommand": "astro build",
"outputDirectory": "dist",
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "s-maxage=60" }
]
}
],
"redirects": [
{ "source": "/old-path", "destination": "/new-path", "permanent": true }
]
}
```
### Deployment
```bash
# Deploy to Vercel
npx vercel
# Production deploy
npx vercel --prod
# Or connect to Git for automatic deploys
```
## Netlify
### Setup
```bash
npx astro add netlify
```
```typescript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';
export default defineConfig({
output: 'server', // or 'hybrid'
adapter: netlify({
edgeMiddleware: true, // Run middleware at the edge
imageCDN: true, // Use Netlify Image CDN
}),
});
```
### Netlify Configuration
```toml
# netlify.toml
[build]
command = "astro build"
publish = "dist"
[build.environment]
NODE_VERSION = "20"
# Redirects
[[redirects]]
from = "/old-path"
to = "/new-path"
status = 301
# Custom headers
[[headers]]
for = "/api/*"
[headers.values]
Access-Control-Allow-Origin = "*"
Cache-Control = "public, max-age=60"
# Netlify Forms
# Forms are auto-detected in static builds
# For SSR, use Netlify Forms API
```
### Netlify Edge Functions
```typescript
// netlify/edge-functions/geolocation.ts
import type { Context } from '@netlify/edge-functions';
export default async function (request: Request, context: Context) {
const { country, city } = context.geo;
// Add geo data to request headers for Astro middleware
request.headers.set('x-country', country?.code ?? 'US');
request.headers.set('x-city', city ?? 'Unknown');
return context.next();
}
export const config = { path: '/*' };
```
### Netlify Forms with Astro
```astro
---
// Static output - Netlify auto-detects forms
---
```
### Deployment
```bash
# Deploy to Netlify
npx netlify deploy
# Production deploy
npx netlify deploy --prod
# Or connect to Git for automatic deploys
```
## Node.js (Self-hosted)
### Setup
```bash
npx astro add node
```
```typescript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone', // or 'middleware'
}),
});
```
### Standalone Mode
```bash
# Build
npm run build
# Run (starts built-in HTTP server)
HOST=0.0.0.0 PORT=4321 node dist/server/entry.mjs
```
### Middleware Mode (Express/Fastify)
```typescript
// astro.config.mjs
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'middleware' }),
});
```
```typescript
// server.mjs - Custom Express server
import express from 'express';
import { handler as astroHandler } from './dist/server/entry.mjs';
const app = express();
// Custom middleware before Astro
app.use('/health', (req, res) => {
res.json({ status: 'ok' });
});
// Serve static files
app.use(express.static('dist/client'));
// Astro handles everything else
app.use(astroHandler);
const port = process.env.PORT || 4321;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
```
```typescript
// server-fastify.mjs - Custom Fastify server
import Fastify from 'fastify';
import fastifyStatic from '@fastify/static';
import { handler as astroHandler } from './dist/server/entry.mjs';
import { fileURLToPath } from 'url';
import path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = Fastify({ logger: true });
// Static files
app.register(fastifyStatic, {
root: path.join(__dirname, 'dist/client'),
});
// Health check
app.get('/health', async () => ({ status: 'ok' }));
// Astro handler
app.use(astroHandler);
app.listen({ port: 4321, host: '0.0.0.0' });
```
### Docker Deployment
```dockerfile
# Dockerfile
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-slim AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:4321/health || exit 1
CMD ["node", "dist/server/entry.mjs"]
```
```yaml
# docker-compose.yml
services:
astro:
build: .
ports:
- "4321:4321"
environment:
- DATABASE_URL=postgres://db:5432/app
- SECRET_KEY=${SECRET_KEY}
restart: unless-stopped
depends_on:
- db
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
pgdata:
```
## Static Hosting
### Configuration
```typescript
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static', // Default - all pages prerendered
site: 'https://example.com',
base: '/my-app', // If hosted at a subpath
});
```
### GitHub Pages
```yaml
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
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
- uses: actions/upload-pages-artifact@v3
with:
path: dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
```
```typescript
// astro.config.mjs for GitHub Pages
export default defineConfig({
site: 'https://username.github.io',
base: '/repo-name', // For project pages (not needed for user pages)
});
```
### S3 + CloudFront
```bash
# Build and sync to S3
npm run build
aws s3 sync dist s3://my-bucket --delete
aws cloudfront create-invalidation --distribution-id DIST_ID --paths "/*"
```
## Environment Variables
### Astro Environment Variable Rules
```
# .env
# Private (server-only) - NOT available in client-side code
DATABASE_URL=postgres://localhost:5432/mydb
API_SECRET=sk-12345
SESSION_KEY=abc
# Public (available in client-side code) - MUST start with PUBLIC_
PUBLIC_API_URL=https://api.example.com
PUBLIC_SITE_NAME=My Site
PUBLIC_GA_ID=G-12345
```
### Accessing Environment Variables
```typescript
// Server-side (pages, middleware, API routes, server islands)
const dbUrl = import.meta.env.DATABASE_URL; // Works
const apiKey = import.meta.env.API_SECRET; // Works
const publicUrl = import.meta.env.PUBLIC_API_URL; // Works
// Client-side (browser, client:* components)
const publicUrl = import.meta.env.PUBLIC_API_URL; // Works
const dbUrl = import.meta.env.DATABASE_URL; // undefined!
```
### envField Schema Validation (Astro 5)
```typescript
// astro.config.mjs
import { defineConfig, envField } from 'astro/config';
export default defineConfig({
env: {
schema: {
// Server-only variables
DATABASE_URL: envField.string({
context: 'server',
access: 'secret',
optional: false,
}),
API_KEY: envField.string({
context: 'server',
access: 'secret',
}),
PORT: envField.number({
context: 'server',
access: 'public',
default: 4321,
}),
// Client-accessible variables
PUBLIC_API_URL: envField.string({
context: 'client',
access: 'public',
}),
PUBLIC_FEATURE_FLAG: envField.boolean({
context: 'client',
access: 'public',
default: false,
}),
},
},
});
```
```typescript
// Type-safe env access with validation
import { DATABASE_URL, PORT } from 'astro:env/server';
import { PUBLIC_API_URL, PUBLIC_FEATURE_FLAG } from 'astro:env/client';
// These are typed and validated at build time
console.log(DATABASE_URL); // string (required)
console.log(PORT); // number (defaults to 4321)
console.log(PUBLIC_API_URL); // string (required)
```
### Platform-specific Environment Variables
```bash
# Cloudflare - set in wrangler.toml or dashboard
wrangler secret put API_KEY
# Vercel
vercel env add API_KEY production
# Netlify
netlify env:set API_KEY "value"
# Docker
docker run -e DATABASE_URL=... my-astro-app
```
## Headers and Redirects
### Middleware-based Headers
```typescript
// src/middleware.ts
import { defineMiddleware, sequence } from 'astro:middleware';
const securityHeaders = defineMiddleware(async (context, next) => {
const response = await next();
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-XSS-Protection', '1; mode=block');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);
return response;
});
const cacheHeaders = defineMiddleware(async (context, next) => {
const response = await next();
// Cache static assets aggressively
if (context.url.pathname.startsWith('/_astro/')) {
response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
}
return response;
});
export const onRequest = sequence(securityHeaders, cacheHeaders);
```
### Static File Headers
```
# public/_headers (Cloudflare Pages / Netlify)
/*
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
/_astro/*
Cache-Control: public, max-age=31536000, immutable
/api/*
Cache-Control: no-cache
Access-Control-Allow-Origin: *
```
### Redirects
```
# public/_redirects (Cloudflare Pages / Netlify)
/old-blog/* /blog/:splat 301
/legacy / 302
/docs https://docs.example.com 301
```
```typescript
// Programmatic redirects in middleware
import { defineMiddleware } from 'astro:middleware';
const redirects: Record = {
'/old-path': { to: '/new-path', status: 301 },
'/legacy': { to: '/', status: 302 },
};
export const onRequest = defineMiddleware(async ({ url, redirect }, next) => {
const rule = redirects[url.pathname];
if (rule) {
return redirect(rule.to, rule.status);
}
return next();
});
```
## SSR Streaming
### Response Streaming
```astro
---
// Astro streams HTML by default in SSR mode
// Components render top-to-bottom, streaming chunks to the client
// Slow data fetch - page header already visible while this loads
const slowData = await fetch('https://slow-api.example.com/data')
.then(r => r.json());
---
Page Title
{slowData.content}
```
### Streaming with Server Islands
```astro
---
// Combine streaming with server islands for optimal loading
import SlowWidget from '../components/SlowWidget.astro';
import UserDashboard from '../components/UserDashboard.astro';
---
Fast static content