Netlify — Deployment
Netlify is a cloud platform that combines global CDN, serverless functions, form handling, and split testing into a single deployment pipeline. It excels at deploying JAMstack sites with instant rollback, deploy previews, and branch-based workflows.
Like Vercel, Netlify connects to your Git repository and auto-deploys on every push. Its unique strengths include native form handling, branch-based split testing, and a flexible redirects engine.
The Netlify CLI provides local development, deployment, and configuration management from the command line.
| 1 | # Install Netlify CLI |
| 2 | npm install -g netlify-cli |
| 3 | |
| 4 | # Login to Netlify |
| 5 | netlify login |
| 6 | |
| 7 | # Initialize a new site |
| 8 | netlify init |
| 9 | |
| 10 | # Link an existing site |
| 11 | netlify link |
| 12 | |
| 13 | # Deploy to a draft URL (preview) |
| 14 | netlify deploy |
| 15 | |
| 16 | # Deploy to production |
| 17 | netlify deploy --prod |
| 18 | |
| 19 | # Run local dev server with functions |
| 20 | netlify dev |
| 21 | |
| 22 | # Open site in browser |
| 23 | netlify open |
| 24 | |
| 25 | # List all sites |
| 26 | netlify sites:list |
The netlify.toml file defines all deployment configuration: build settings, redirects, headers, functions, and split testing. It belongs at the root of your repository.
| 1 | [build] |
| 2 | command = "npm run build" |
| 3 | publish = "dist" |
| 4 | functions = "netlify/functions" |
| 5 | |
| 6 | [build.environment] |
| 7 | NODE_VERSION = "20" |
| 8 | NPM_VERSION = "10" |
| 9 | |
| 10 | # Redirects |
| 11 | [[redirects]] |
| 12 | from = "/api/*" |
| 13 | to = "/.netlify/functions/:splat" |
| 14 | status = 200 |
| 15 | |
| 16 | [[redirects]] |
| 17 | from = "/old-path" |
| 18 | to = "/new-path" |
| 19 | status = 301 |
| 20 | |
| 21 | # Headers |
| 22 | [[headers]] |
| 23 | for = "/*" |
| 24 | [headers.values] |
| 25 | X-Frame-Options = "DENY" |
| 26 | X-Content-Type-Options = "nosniff" |
| 27 | Referrer-Policy = "strict-origin-when-cross-origin" |
| 28 | |
| 29 | [[headers]] |
| 30 | for = "/assets/*" |
| 31 | [headers.values] |
| 32 | Cache-Control = "public, max-age=31536000, immutable" |
| 33 | |
| 34 | # Split Testing |
| 35 | [[split_testing]] |
| 36 | branches = ["main", "experiment"] |
| 37 | percentage = 90 |
| 38 | |
| 39 | # Edge Functions |
| 40 | [[edge_functions]] |
| 41 | function = "hello" |
| 42 | path = "/hello" |
| 43 | |
| 44 | # Environment Variables (per context) |
| 45 | [context.production.environment] |
| 46 | API_URL = "https://api.example.com" |
| 47 | |
| 48 | [context.deploy-preview.environment] |
| 49 | API_URL = "https://staging-api.example.com" |
| 50 | |
| 51 | [context.branch-deploy.environment] |
| 52 | API_URL = "https://dev-api.example.com" |
info
Netlify creates a unique preview URL for every deploy, including automatic PR comments. This enables visual review and testing before merging.
| 1 | # Deploy preview URL pattern: |
| 2 | https://[hash]--[site-name].netlify.app |
| 3 | |
| 4 | # Examples: |
| 5 | https://a1b2c3d4--my-app.netlify.app |
| 6 | https://e5f6g7h8--my-app.netlify.app |
| 7 | |
| 8 | # Deploy a branch-specific preview |
| 9 | netlify deploy --branch feat-new-ui |
| 10 | |
| 11 | # Deploy with alias for consistent URL |
| 12 | netlify deploy --alias staging |
| 13 | |
| 14 | # Lock a deploy to prevent auto-overwrite |
| 15 | netlify deploy --prod --message "v2.0.0 release" |
pro tip
Netlify Functions run serverless code in response to HTTP requests. Functions are stored in the directory specified by functions in netlify.toml (default: netlify/functions).
| 1 | // netlify/functions/hello.ts |
| 2 | import { Handler } from '@netlify/functions'; |
| 3 | |
| 4 | export const handler: Handler = async (event, context) => { |
| 5 | const { name = 'World' } = event.queryStringParameters || {}; |
| 6 | |
| 7 | return { |
| 8 | statusCode: 200, |
| 9 | headers: { |
| 10 | 'Content-Type': 'application/json', |
| 11 | }, |
| 12 | body: JSON.stringify({ |
| 13 | message: `Hello, ${name}!`, |
| 14 | path: event.path, |
| 15 | httpMethod: event.httpMethod, |
| 16 | }), |
| 17 | }; |
| 18 | }; |
| 19 | |
| 20 | // Function with environment variables |
| 21 | // netlify/functions/submit-form.ts |
| 22 | export const handler: Handler = async (event) => { |
| 23 | if (event.httpMethod !== 'POST') { |
| 24 | return { statusCode: 405, body: 'Method Not Allowed' }; |
| 25 | } |
| 26 | |
| 27 | const body = JSON.parse(event.body || '{}'); |
| 28 | const apiKey = process.env.FORM_API_KEY; |
| 29 | |
| 30 | const response = await fetch('https://api.example.com/submit', { |
| 31 | method: 'POST', |
| 32 | headers: { |
| 33 | Authorization: `Bearer ${apiKey}`, |
| 34 | 'Content-Type': 'application/json', |
| 35 | }, |
| 36 | body: JSON.stringify(body), |
| 37 | }); |
| 38 | |
| 39 | return { |
| 40 | statusCode: response.status, |
| 41 | body: await response.text(), |
| 42 | }; |
| 43 | }; |
Function limits:
| Property | Free | Pro |
|---|---|---|
| Memory | 1024 MB | 1024 MB |
| Execution Time | 10s | 60s (extendable to 900s) |
| Invocations | 125K/month | 2M/month |
| Bundle Size | 50 MB | 250 MB |
Netlify offers two ways to configure redirects and headers: the _redirects file and netlify.toml. The _redirects file lives in the publish directory and uses a simpler syntax.
| 1 | # _redirects — Simple syntax |
| 2 | # Format: [from] [to] [status] |
| 3 | |
| 4 | # Redirect all /api/* requests to serverless functions |
| 5 | /api/* /.netlify/functions/:splat 200 |
| 6 | |
| 7 | # Permanent redirects (301) |
| 8 | /old-blog /blog 301 |
| 9 | |
| 10 | # Splat parameters |
| 11 | /blog/:slug /article/:slug 301 |
| 12 | |
| 13 | # Country-based redirects |
| 14 | /* /europe /eu 302 Country=de,fr,it |
| 15 | |
| 16 | # Language-based redirects |
| 17 | /* /spanish /es 302 Accept-Language=es |
| 18 | |
| 19 | # Query parameter redirects |
| 20 | /shop /sale 302 q=clearance |
| 21 | |
| 22 | # SPA fallback (serve index.html for all routes) |
| 23 | /* /index.html 200 |
Equivalent configuration in netlify.toml:
| 1 | # netlify.toml redirects (equivalent to _redirects above) |
| 2 | |
| 3 | [[redirects]] |
| 4 | from = "/api/*" |
| 5 | to = "/.netlify/functions/:splat" |
| 6 | status = 200 |
| 7 | |
| 8 | [[redirects]] |
| 9 | from = "/old-blog/*" |
| 10 | to = "/blog/:splat" |
| 11 | status = 301 |
| 12 | |
| 13 | # SPA fallback |
| 14 | [[redirects]] |
| 15 | from = "/*" |
| 16 | to = "/index.html" |
| 17 | status = 200 |
| 18 | |
| 19 | # Splat / named parameters |
| 20 | [[redirects]] |
| 21 | from = "/blog/:slug" |
| 22 | to = "/article/:slug" |
| 23 | status = 301 |
Netlify Edge Functions run at the CDN edge, globally distributed, with near-zero cold starts. They use Deno runtime and can modify requests and responses in flight.
| 1 | // netlify/edge-functions/hello.ts |
| 2 | import type { Context } from '@netlify/edge-functions'; |
| 3 | |
| 4 | export default async (request: Request, context: Context) => { |
| 5 | const url = new URL(request.url); |
| 6 | |
| 7 | // Geolocation data available at the edge |
| 8 | const country = context.geo.country?.code || 'unknown'; |
| 9 | const city = context.geo.city || 'unknown'; |
| 10 | |
| 11 | // A/B testing based on cookie |
| 12 | const experiment = context.cookies.get('experiment'); |
| 13 | if (!experiment) { |
| 14 | context.cookies.set({ |
| 15 | name: 'experiment', |
| 16 | value: Math.random() > 0.5 ? 'A' : 'B', |
| 17 | }); |
| 18 | } |
| 19 | |
| 20 | // Rewrite based on country |
| 21 | if (country === 'DE' && !url.pathname.startsWith('/de')) { |
| 22 | return new Response(null, { |
| 23 | status: 302, |
| 24 | headers: { Location: `/de${url.pathname}` }, |
| 25 | }); |
| 26 | } |
| 27 | |
| 28 | return context.next(); |
| 29 | }; |
| 30 | |
| 31 | // netlify/edge-functions/geolocation.ts |
| 32 | export default async (request: Request, context: Context) => { |
| 33 | const response = await context.next(); |
| 34 | const text = await response.text(); |
| 35 | |
| 36 | // Inject geolocation data into the HTML |
| 37 | const injected = text.replace( |
| 38 | '</body>', |
| 39 | `<script> |
| 40 | window.GEO = ${JSON.stringify(context.geo)}; |
| 41 | </script></body>` |
| 42 | ); |
| 43 | |
| 44 | return new Response(injected, response); |
| 45 | }; |