AstroShowcase
Docs / Deployment / Environment Variables
Deployment Updated May 7, 2026

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:

PrefixWhere it’s availableUse for
PUBLIC_Server and browserPublic config (site URL, analytics ID)
(no prefix)Server onlySecrets (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:

FileWhen loaded
.envAlways
.env.localAlways (gitignored by default)
.env.developmentDuring npm run dev only
.env.productionDuring npm run build only
.env.development.localdev + local overrides
.env.production.localbuild + local overrides

Lower files in the table take priority over higher ones.