Deploy Astro to Netlify Free Tier (Config That Actually Works)
netlify.toml, build settings, environment variables, and fixing the 'Page not found' on refresh bug.
Tested on: Astro 5.7, Netlify free tier, Node 22 Deploy time: ~90 seconds. Cost: $0.
netlify.toml
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "22"
[[headers]]
for = "/_astro/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
package.json scripts
{
"scripts": {
"build": "astro build",
"preview": "astro preview"
}
}
Netlify auto-detects Astro sometimes. Don't trust it — explicit netlify.toml wins.
Environment variables
Site settings → Environment variables:
PUBLIC_SITE_URL=https://myblog.netlify.app
astro.config.mjs:
export default defineConfig({
site: import.meta.env.PUBLIC_SITE_URL || 'http://localhost:4321'
});
RSS feeds and canonical URLs need the production URL.
Error — build succeeds, site blank
Publish directory was set to .astro instead of dist
Netlify UI overrides netlify.toml if you set publish dir manually. Clear UI setting or match dist.
Error — 404 on direct URL access
Astro static outputs about/index.html. Netlify serves it for /about and /about/.
If using output: 'server' or hybrid — different story. For static:
export default defineConfig({ output: 'static' });
No _redirects SPA fallback needed.
Deploy preview
Connect GitHub repo. Every PR gets preview URL. Check:
curl -I https://deploy-preview-42--myblog.netlify.app/about/
Custom domain (free tier)
Domain settings → Add custom domain → Netlify DNS or external CNAME to myblog.netlify.app.
HTTPS auto-provisioned via Let's Encrypt.
Build minute budget
Free tier: 300 build minutes/month
My Astro site build: ~1.5 min
~200 deploys/month headroom
Checklist
-
publish = "dist" -
sitein astro.config.mjs -
NODE_VERSIONpinned -
/_astro/*cache headers - Test one deep link after deploy