Environment Variables
How to manage secrets, API keys, and config in Astro — .env files, import.meta.env, and what never to expose to the browser.
How environment variables work in Astro
Astro uses .env files and the import.meta.env object — the same pattern as Vite.
Variables are split into two categories:
| Prefix | Where it’s available | Use for |
|---|---|---|
PUBLIC_ | Server and browser | Public config (site URL, analytics ID) |
| (no prefix) | Server only | Secrets (API keys, DB passwords) |
Variables without PUBLIC_ are stripped from the browser bundle entirely.
They never reach the client even if you accidentally use them in a component.
Setting up .env files
Create a .env file at the project root (never commit this to git):
# .env
DB_HOST=localhost
DB_PORT=3306
DB_NAME=my_database
DB_USER=my_user
DB_PASSWORD=supersecret
GEMINI_API_KEY=AIza...
SCREENSHOT_API_KEY=abc123
# Public vars — safe to expose to the browser
PUBLIC_SITE_URL=https://my-site.com
PUBLIC_GA_ID=G-XXXXXXXXX
Create a .env.example with placeholders (safe to commit — it documents what’s needed):
# .env.example
DB_HOST=localhost
DB_PORT=3306
DB_NAME=
DB_USER=
DB_PASSWORD=
GEMINI_API_KEY=
SCREENSHOT_API_KEY=
PUBLIC_SITE_URL=https://your-domain.com
Accessing variables
---
// In any .astro file, API route, or server-side library
const dbHost = import.meta.env.DB_HOST // server only
const siteUrl = import.meta.env.PUBLIC_SITE_URL // server + browser
const isDev = import.meta.env.DEV // true during npm run dev
const isProd = import.meta.env.PROD // true after npm run build
const mode = import.meta.env.MODE // 'development' or 'production'
---
In plain TypeScript files (libraries, API routes):
// src/lib/db.ts
const host = import.meta.env.DB_HOST
const port = Number(import.meta.env.DB_PORT ?? 3306)
Type safety for your variables
Create (or extend) src/env.d.ts to get autocomplete and type errors:
/// <reference types="astro/client" />
interface ImportMetaEnv {
readonly DB_HOST: string
readonly DB_PORT: string
readonly DB_NAME: string
readonly DB_USER: string
readonly DB_PASSWORD: string
readonly GEMINI_API_KEY: string
readonly SCREENSHOT_API_KEY: string
readonly PUBLIC_SITE_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
Now TypeScript will warn you if you typo a variable name.
Loading .env in production
When running the Node.js standalone server in production, environment variables
are not automatically loaded from .env. You have two options:
Option 1 — dotenv package (recommended)
npm install dotenv
// src/lib/env.ts — import this at the top of any file that needs env vars
import 'dotenv/config'
Or load it in your PM2 ecosystem.config.cjs:
require('dotenv').config()
module.exports = {
apps: [{
name: 'my-site',
script: './dist/server/entry.mjs',
// ...
}]
}
Option 2 — Set vars directly in ecosystem.config.cjs
module.exports = {
apps: [{
name: 'my-site',
script: './dist/server/entry.mjs',
env: {
NODE_ENV: 'production',
DB_HOST: 'localhost',
DB_PASSWORD: process.env.DB_PASSWORD, // read from shell env
}
}]
}
Never expose secrets
These common mistakes expose secrets to the browser — Astro will warn you, but it’s worth knowing:
---
// ✗ BAD — variable has no PUBLIC_ prefix but is used in template
// Astro will NOT send this to the browser, but it's confusing
const key = import.meta.env.SECRET_KEY
---
<!-- ✗ BAD — if you somehow render this, users can see it in page source -->
<p>{key}</p>
---
// ✓ GOOD — use the secret server-side only
const key = import.meta.env.SECRET_KEY
const result = await callApiWithKey(key) // result is safe to display
---
<p>{result.publicData}</p>
.env file hierarchy
Astro follows the same .env loading order as Vite:
| File | When loaded |
|---|---|
.env | Always |
.env.local | Always (gitignored by default) |
.env.development | During npm run dev only |
.env.production | During npm run build only |
.env.development.local | dev + local overrides |
.env.production.local | build + local overrides |
Lower files in the table take priority over higher ones.