Skip to content
Codeloom
Astro

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.

·7 min read · By Codeloom
Intermediate 13 min read

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

  1. Function size limits: Serverless functions have a 50MB compressed limit. If your dependencies are large, use functionPerRoute: true to split into separate functions.
  2. Cold starts: Serverless functions have cold start latency. Use ISR for pages that do not need real-time data.
  3. Edge runtime limitations: Edge functions cannot use Node.js-specific APIs like fs or crypto.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

  1. Function timeout: Default is 10 seconds (26 seconds on Pro). Long database queries will time out.
  2. Build minutes: Free tier has limited build minutes. Cache node_modules and .astro/ to speed up builds.
  3. 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

  1. Workers runtime: Cloudflare Pages runs on the Workers runtime, not Node.js. Many Node.js APIs are unavailable. Use the nodejs_compat compatibility flag for basic support.
  2. Request limits: Free tier allows 100,000 requests/day. Workers Paid is $5/month for 10 million requests.
  3. 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:

  1. Run astro check to catch type errors
  2. Run astro build locally to verify the build succeeds
  3. Check that all environment variables are set on the target platform
  4. Verify your site URL in astro.config.mjs matches your production domain
  5. Test redirects and rewrites
  6. Check that robots.txt and sitemap.xml are generated correctly
  7. 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.