| name | netlify-config |
| description | Reference for netlify.toml configuration and site environment variables. Use when configuring build settings, redirects, rewrites, headers, deploy contexts, the `[dev]` block that controls `netlify dev` (command, port, targetPort, framework), or any site-level configuration — and when managing environment variables or secrets with the Netlify CLI, including scoping values to specific deploy contexts. Covers the complete netlify.toml syntax including redirects with splats/conditions, headers, deploy contexts, functions config, edge functions config, and the `[dev]` block (including when `framework` must be `"#custom"` — required when both a custom `command` and `targetPort` are set). |
Netlify Configuration (netlify.toml)
Place netlify.toml at the repository root. In a monorepo, Netlify searches for the config and uses the first one it finds: the package directory (the subdirectory containing the site), then the base directory, then the repository root — so put a site-specific netlify.toml in the package directory to take precedence over root-level config (see the netlify-deploy skill's monorepo notes).
netlify.toml takes precedence over the Netlify UI. When the same property (build command, publish directory, an environment variable, a redirect, a header) is configured in both places, the value in netlify.toml wins and silently overrides the corresponding Netlify UI setting — the dashboard field still shows its old value but is inert. Once a netlify.toml is present, treat it as the source of truth and change settings there, not in the UI.
Build Settings
[build]
base = "project/"
command = "npm run build"
publish = "dist/"
Redirects
[[redirects]]
from = "/old"
to = "/new"
status = 301
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[redirects]]
from = "/blog/*"
to = "/news/:splat"
[[redirects]]
from = "/users/:id"
to = "/api/users/:id"
status = 200
[[redirects]]
from = "/app/*"
to = "/index.html"
status = 200
force = true
[[redirects]]
from = "/api/*"
to = "https://api.example.com/:splat"
status = 200
[redirects.headers]
X-Custom = "value"
[[redirects]]
from = "/*"
to = "/fr/:splat"
status = 200
conditions = { Country = ["FR"], Language = ["fr"] }
Rule order matters — Netlify processes the first matching rule. Place specific rules before general ones.
Redirect rules can also live in a plain-text _redirects file in the publish directory. If both a _redirects file and [[redirects]] in netlify.toml exist, the _redirects file rules are processed first, then the netlify.toml rules, reading top to bottom — and the first matching rule wins. The same ordering applies to a _headers file versus [[headers]]. Because a _redirects rule can silently shadow a netlify.toml rule for the same path, keep overlapping rules in a single source.
Headers
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
Headers apply only to files served from Netlify's CDN (not to function or edge function responses — set those in code).
Deploy Contexts
Override settings per deploy context:
[context.production]
command = "npm run build"
environment = { NODE_ENV = "production" }
[context.deploy-preview]
command = "npm run build:preview"
[context.branch-deploy]
command = "npm run build:staging"
[context.dev]
environment = { NODE_ENV = "development" }
[context."staging"]
command = "npm run build:staging"
[[redirects]] and [[headers]] are global — they cannot be scoped to a deploy context. Context tables like [context.production] work for keys such as [build], [build.environment], and [[plugins]], but redirect and header rules apply to every context no matter where you place them in the file; there is no [context.production.redirects] or context-nested [[redirects]]. For context-specific redirects or headers, use the per-deploy escape hatch: generate a _redirects or _headers file during that context's build (those files ship per deploy), or gate the behavior on a runtime signal in an edge function.
Environment Variables
[build.environment]
NODE_VERSION = "20"
[context.production.environment]
API_URL = "https://api.prod.com"
[context.deploy-preview.environment]
API_URL = "https://api.staging.com"
Do not put secrets in netlify.toml (it's committed to source control). Use the Netlify UI or CLI for sensitive values — see CLI Management below.
Variables declared in netlify.toml are build-scoped only. Values under [build.environment] or [context.*.environment] are available to the build (and snippet injection) but are not injected into the Functions or Edge Functions runtime — reading them with Netlify.env.get("VAR") or process.env.VAR inside a function returns undefined. To make a variable available at function runtime, set it in the Netlify UI or with netlify env:set (those are available to both builds and runtime), not in netlify.toml.
CLI Management
netlify env:set API_KEY "value"
netlify env:set API_KEY "value" --secret
netlify env:set API_KEY "value" --context production
netlify env:get API_KEY
netlify env:list
netlify env:list --plain > .env
netlify env:import .env
netlify env:unset API_KEY
Never put secrets in client-prefixed variables (VITE_, PUBLIC_, NEXT_PUBLIC_, NUXT_PUBLIC_) — these are inlined into the client bundle and exposed to the browser. --secret only hides a value from logs and the UI; it does not protect a client-prefixed variable.
Context Scoping
Variables set via the CLI can also be scoped to deploy contexts:
netlify env:set API_URL "https://api.prod.com" --context production
netlify env:set API_URL "https://api.staging.com" --context deploy-preview
netlify env:set DEBUG "true" --context branch:feature-x
This is the CLI equivalent of the [context.*.environment] tables above, but the resulting variables are available at both build and runtime (unlike netlify.toml-declared ones).
When reading these variables in server code, prefer Netlify.env.get("VAR"). process.env.VAR also works inside Functions, but Edge Functions expose only Netlify.env.get — the portable form keeps the same code working in both runtimes.
For the client-side rules (VITE_/PUBLIC_ prefixes and framework specifics), see the netlify-frameworks skill; for function-side details, see netlify-functions.
Functions Configuration
[functions]
directory = "netlify/functions"
node_bundler = "esbuild"
[functions."cleanup"]
schedule = "@daily"
Use the single-table [functions] form for global settings and [functions."name-or-glob"] for per-function overrides. There is no [[functions]] array-of-tables and no path-based function routing table in netlify.toml — functions are routed by file (served at /.netlify/functions/{name}) or by an in-code path/config export, not by config.
Edge Functions Configuration
[[edge_functions]]
path = "/admin/*"
function = "auth"
excludedPath = "/admin/public/*"
[functions]
deno_import_map = "./import_map.json"
Dev Server
[dev]
command = "npm start"
port = 8888
targetPort = 3000
framework = "#auto"
If you set both a custom command and a targetPort, framework must be "#custom". With framework = "#auto" (the default) Netlify Dev runs its own detector and ignores your custom command; "#custom" tells it to run your command as the app server and connect to targetPort. Setting command + targetPort while leaving framework at #auto (or omitting it) is a silent misconfiguration.
Plugins
[[plugins]]
package = "@netlify/plugin-lighthouse"
[plugins.inputs]
audits = ["performance", "accessibility"]
Image CDN
[images]
remote_images = ["https://example\\.com/.*"]
See the netlify-image-cdn skill for full Image CDN usage.