Deploying Astro: Vercel, Netlify, Cloudflare, and Docker
A practical guide to deploying Astro projects across major platforms: adapter configuration, environment variables, build settings, and platform-specific gotchas.
What you'll learn
- ✓How Astro adapters connect your project to deployment platforms
- ✓How to deploy a static Astro site to any CDN
- ✓How to deploy SSR Astro to Vercel, Netlify, and Cloudflare
- ✓How to containerize Astro with Docker for self-hosting
- ✓Platform-specific configuration and common deployment issues
Prerequisites
- •A working Astro project
- •Basic Git and CLI knowledge
Static vs SSR: Know Your Output Mode
Before deploying, decide what your site needs:
- Static (default): Every page is pre-rendered at build time. Deploy the
dist/folder to any CDN. No server needed. - Hybrid: Most pages are static, but some are server-rendered. Requires an adapter.
- Server: All pages are server-rendered on every request. Requires an adapter.
// astro.config.mjs
export default defineConfig({
output: 'static', // or 'hybrid' or 'server'
});
If your site is purely static, skip the adapter section and deploy the dist/ folder anywhere.
Deploying to Vercel
Setup
npx astro add vercel
This installs @astrojs/vercel and updates your config:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel';
export default defineConfig({
output: 'server',
adapter: vercel(),
});
Configuration Options
adapter: vercel({
// Enable Vercel Web Analytics
webAnalytics: { enabled: true },
// Enable Image Optimization API
imageService: true,
// Enable ISR (Incremental Static Regeneration)
isr: {
expiration: 60, // Revalidate every 60 seconds
},
// Edge Functions (runs at CDN edge, limited Node.js APIs)
functionPerRoute: false,
}),
Environment Variables
Set them in the Vercel dashboard under Settings > Environment Variables, or use the CLI:
vercel env add DATABASE_URL production
vercel env add GITHUB_CLIENT_SECRET production preview
In your Astro code, access them with import.meta.env:
const dbUrl = import.meta.env.DATABASE_URL;
Deploy
# Install Vercel CLI
npm i -g vercel
# Preview deployment
vercel
# Production deployment
vercel --prod
Or connect your GitHub repo in the Vercel dashboard for automatic deploys on push.
Vercel-Specific Gotchas
- Function size limits: Serverless functions have a 50MB compressed limit. If your dependencies are large, use
functionPerRoute: trueto split into separate functions. - Cold starts: Serverless functions have cold start latency. Use ISR for pages that do not need real-time data.
- Edge runtime limitations: Edge functions cannot use Node.js-specific APIs like
fsorcrypto.scrypt. Use the default Node.js runtime unless you need edge latency.
Deploying to Netlify
Setup
npx astro add netlify
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';
export default defineConfig({
output: 'server',
adapter: netlify(),
});
Configuration
adapter: netlify({
// Use edge functions instead of serverless
edgeMiddleware: true,
// Cache pages with On-Demand Builders
cacheOnDemandPages: true,
}),
netlify.toml
# netlify.toml
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "20"
# Redirect www to non-www
[[redirects]]
from = "https://www.example.com/*"
to = "https://example.com/:splat"
status = 301
force = true
# Custom headers
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Environment Variables
Set them in the Netlify dashboard under Site settings > Environment variables:
# Or use the CLI
netlify env:set DATABASE_URL "postgres://..."
Deploy
# Install Netlify CLI
npm i -g netlify-cli
# Link to your site
netlify link
# Preview deploy
netlify deploy
# Production deploy
netlify deploy --prod
Netlify-Specific Gotchas
- Function timeout: Default is 10 seconds (26 seconds on Pro). Long database queries will time out.
- Build minutes: Free tier has limited build minutes. Cache
node_modulesand.astro/to speed up builds. - Large files: Files over 10MB in
dist/are not supported on the CDN. Use Netlify Large Media or an external storage service.
Deploying to Cloudflare Pages
Setup
npx astro add cloudflare
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare(),
});
Configuration
adapter: cloudflare({
// Access Cloudflare bindings (KV, D1, R2)
platformProxy: {
enabled: true,
},
// Use node_compat for Node.js API compatibility
runtime: {
mode: 'local',
type: 'pages',
},
}),
Using Cloudflare Bindings
Cloudflare’s unique advantage is direct access to KV, D1 (SQLite), R2 (object storage), and other bindings:
// src/pages/api/data.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ locals }) => {
const runtime = locals.runtime;
// KV storage
const value = await runtime.env.MY_KV.get('key');
// D1 database
const { results } = await runtime.env.DB.prepare(
'SELECT * FROM users WHERE active = ?',
).bind(1).all();
return new Response(JSON.stringify({ value, users: results }), {
headers: { 'Content-Type': 'application/json' },
});
};
wrangler.toml
# wrangler.toml
name = "my-astro-site"
compatibility_date = "2026-07-01"
[vars]
API_KEY = "public-value"
[[kv_namespaces]]
binding = "MY_KV"
id = "abc123"
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "def456"
Deploy
# Install Wrangler
npm i -g wrangler
# Deploy
wrangler pages deploy dist/
Or connect your GitHub repo in the Cloudflare dashboard.
Cloudflare-Specific Gotchas
- Workers runtime: Cloudflare Pages runs on the Workers runtime, not Node.js. Many Node.js APIs are unavailable. Use the
nodejs_compatcompatibility flag for basic support. - Request limits: Free tier allows 100,000 requests/day. Workers Paid is $5/month for 10 million requests.
- Build output size: Maximum 25,000 files and 25MB per file.
Deploying with Docker
For self-hosting on any VPS, Kubernetes, or cloud provider:
Dockerfile
# Build stage
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime stage
FROM node:20-alpine AS runtime
WORKDIR /app
# Copy only what's needed
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=4321
EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"]
For Static Sites
If your site is static, use an Nginx image instead:
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
# nginx.conf
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# SPA-style fallback for clean URLs
location / {
try_files $uri $uri/index.html =404;
}
# Cache static assets aggressively
location /_astro/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options "nosniff";
# Gzip
gzip on;
gzip_types text/html text/css application/javascript application/json image/svg+xml;
}
}
docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "4321:4321"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/app
- NODE_ENV=production
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Build and Run
# Build the image
docker build -t my-astro-site .
# Run it
docker run -p 4321:4321 --env-file .env my-astro-site
# Or with docker-compose
docker compose up -d
Deployment Checklist
Before every production deployment:
- Run
astro checkto catch type errors - Run
astro buildlocally to verify the build succeeds - Check that all environment variables are set on the target platform
- Verify your
siteURL inastro.config.mjsmatches your production domain - Test redirects and rewrites
- Check that
robots.txtandsitemap.xmlare generated correctly - Run Lighthouse on a preview deployment before promoting to production
Summary
Astro deploys anywhere. Static sites go to any CDN with zero configuration. SSR sites need an adapter, and each platform — Vercel, Netlify, Cloudflare — has its own adapter with platform-specific features like edge functions, KV storage, and ISR. Docker gives you full control for self-hosting. The key is matching your output mode to your deployment target and understanding the constraints of each platform.
Related articles
- Astro Astro Integrations and Adapters: An Overview
Understand the difference between Astro integrations and adapters, when to reach for each, and how they shape your deployment pipeline.
- Airflow Deploying Apache Airflow to Production
Run Airflow in production with Docker Compose, Helm on Kubernetes, or managed services. Covers monitoring, logging, security, and database backends.
- FastAPI Production Deployment of FastAPI with Docker and Gunicorn
Deploy FastAPI to production with Docker, Gunicorn, Uvicorn workers, health checks, multi-stage builds, and best practices.
- Next.js Deploying Next.js on Vercel: A Practical Guide
A hands-on walkthrough for shipping a Next.js app to Vercel — connecting Git, configuring environment variables, understanding preview deployments, and avoiding the usual production gotchas.