> ## Documentation Index
> Fetch the complete documentation index at: https://docs.instapods.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Environment Variables

> Configure your app with environment variables and secrets.

Environment variables are the standard way to pass configuration and secrets (database credentials, API keys, feature flags) to your app without hardcoding them.

<Note>
  Running a [1-Click App](/guides/one-click-apps)? Use `instapods env set` - it writes to the file
  the app reads, puts the values in the app's process environment, and restarts the app. See
  [Configuring an app with environment variables](/guides/one-click-apps#configuring-an-app-with-environment-variables)
  for the one app that cannot take them. The `.env`-in-app-directory detail below is for custom-code
  pods (Static, PHP, Node.js, Python, Go).
</Note>

## Setting Environment Variables

### Option 1: The `instapods env` command (Recommended)

Set one or more variables straight from the CLI:

```bash theme={null}
instapods env set my-app DATABASE_URL=postgres://instapod:secret@localhost:5432/instapod API_KEY=sk-your-api-key-here
```

List or remove them later (values are masked unless you pass `--show-values`):

```bash theme={null}
instapods env list my-app
instapods env list my-app --show-values
instapods env unset my-app API_KEY
```

For custom-code pods this writes to `/home/instapod/app/.env` and restarts the app service. For [1-Click Apps](/guides/one-click-apps) it targets the app's own config file and restarts the app. Either way you do not need to run `reload` or `restart` afterwards.

<Tip>
  If `instapods env list` shows the variable but your app says it is unset, run `instapods env list`
  again and read the top of the output: it prints a `!` line whenever the values are **not** reaching
  your app, and says what to do. Details in [When the values are not
  reaching your app](#when-the-values-are-not-reaching-your-app) below.
</Tip>

### Option 2: Create a `.env` File

The manual approach — create a `.env` file in your app directory via the CLI:

```bash theme={null}
instapods exec my-app -- "cat > /home/instapod/app/.env << 'EOF'
DATABASE_URL=postgres://instapod:secret@localhost:5432/instapod
API_KEY=sk-your-api-key-here
NODE_ENV=production
EOF"
```

Nothing restarts your app when you write the file this way, so reload afterwards:

```bash theme={null}
instapods pods reload my-app
```

Option 1 is still the better choice for anything with a `#`, a quote or a newline in it: `env set`
quotes the value so it reads back exactly as typed, and a heredoc does not.

### Option 3: Set at Pod Creation (API)

Pass environment variables when creating a pod via the API:

```bash theme={null}
curl -X POST https://app.instapods.com/api/pods \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-app",
    "preset": "nodejs",
    "customizations": {
      "env_vars": {
        "NODE_ENV": "production",
        "LOG_LEVEL": "info"
      }
    }
  }'
```

Variables set this way are written to `/etc/environment`, which login shells read. **Systemd does not read `/etc/environment`**, so your app service does not inherit them. For anything your app needs to see, use `instapods env set` (Option 1) — those land in `.env` and reach your app.

## How your app reads them

`instapods env set` writes `KEY=value` into `/home/instapod/app/.env` and restarts your app. Your app
then gets those values **two** ways, and either one is enough:

1. **We put them in the process environment for you.** The app's systemd unit reads a copy of your
   `.env` that InstaPods generates, so `process.env.API_KEY` / `os.environ["API_KEY"]` /
   `os.Getenv("API_KEY")` work with no library and no code.
2. **Your app reads the file itself**, with `dotenv`, `load_dotenv()`, `godotenv.Load()`, or the
   support already built into Laravel, Symfony, Next and Vite.

If your app does both, they agree - a dotenv loader never overwrites a variable that is already set,
and we generate our copy with the same parser your loader uses, so the value is the same either way.

| Preset | Env file | What your app needs |
| - | - | - |
| `nodejs` | `/home/instapod/app/.env` | Nothing. `require('dotenv').config()` also works |
| `python` | `/home/instapod/app/.env` | Nothing. `load_dotenv()` also works |
| `go` | `/home/instapod/app/.env` | Nothing. `godotenv.Load()` also works |
| `php` | `/home/instapod/app/.env` | Nothing. Laravel and Symfony read the file too |
| `static` | n/a | No server-side code, so nothing reads env vars. Use a `config.js` file |
| 1-Click Apps | The app's own file, or `/home/instapod/app/.env` for apps that have none | Nothing. See [the 1-Click Apps guide](/guides/one-click-apps#configuring-an-app-with-environment-variables) |

### Three cases where you still need to read the file yourself

The generated copy is deliberately conservative. It leaves a variable out rather than risk handing
your app a value that is subtly different from what you typed. A variable is skipped when:

* **It is one we manage.** `PORT`, `HOST`, `HOSTNAME` and `NODE_ENV` are set by InstaPods so the
  proxy can reach your app. Setting them yourself is ignored on purpose - if it were not, your app
  would bind somewhere we do not route to and stop serving traffic.
* **Its value contains both an apostrophe and a newline** (or a carriage return). That combination
  is the one shape we cannot guarantee systemd reads identically to dotenv, so we leave it out
  rather than risk corrupting it. A multi-line key or JSON credential with **no** apostrophe is
  passed through fine.
* **You edited `.env` by hand** - over SSH, in the Web IDE, or with `instapods exec`. We cannot tell
  what changed, so we switch the generated copy off for that pod until your next `env set` or
  dashboard save. Your app keeps working; it just has to read the file itself until then.

In all three cases the value is still in `/home/instapod/app/.env`, so a `dotenv.config()` or
`load_dotenv()` in your app covers you. Adding one is never wrong.

The rest of this section shows that code for each stack.

### Node.js

The systemd service sets `NODE_ENV=production` by default. That one comes from the unit itself, and
a value you set for it in `.env` is ignored.

`process.env.DATABASE_URL` already works with no library. Use the `dotenv` package if you also want
the file read directly (see the three cases above):

```javascript theme={null}
// At the top of your entry point (index.js)
require('dotenv').config();

// Access variables
const dbUrl = process.env.DATABASE_URL;
const apiKey = process.env.API_KEY;
```

```bash theme={null}
npm install dotenv
```

On Node 20 and newer you can skip the package and start your app with the flag instead:

```json theme={null}
{
  "scripts": {
    "start": "node --env-file=.env index.js"
  }
}
```

<Note>
  If your pod was created before mid-2026, some values may still live in `/home/instapod/app/.env.local`
  from an older layout. `instapods env set` keeps both files in step for the keys you set, so a stale
  `.env.local` cannot shadow them at build time. New keys go to `.env`.
</Note>

### Python

Use `python-dotenv`:

```python theme={null}
# app.py
from dotenv import load_dotenv
import os

load_dotenv()  # loads .env from current directory

db_url = os.environ.get('DATABASE_URL')
api_key = os.environ.get('API_KEY')
```

```
# requirements.txt
python-dotenv>=1.0
flask>=3.0
```

### PHP

PHP-FPM provides access to system environment variables via `getenv()`. For `.env` files, parse them manually or use a library:

```php theme={null}
<?php
// Simple .env loading (no library needed)
$envFile = __DIR__ . '/../.env';
if (file_exists($envFile)) {
    foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
        if ($line[0] === '#') continue;
        if (strpos($line, '=') !== false) putenv(trim($line));
    }
}

$dbPass = getenv('DB_PASS') ?: '';
$apiKey = getenv('API_KEY') ?: '';
```

For Laravel and Symfony the framework reads `.env` for you, so there is nothing to add.

<Note>
  PHP pods have no `app` service, so `instapods env set` has nothing to restart - PHP re-reads the
  file on the next request. The exception is a cached Laravel config: if you have run
  `php artisan config:cache`, run it again after changing a variable, or the cached values keep
  winning.
</Note>

### Go

Use [`godotenv`](https://github.com/joho/godotenv):

```go theme={null}
// main.go
package main

import (
    "log"
    "os"

    "github.com/joho/godotenv"
)

func main() {
    // Loads /home/instapod/app/.env; the app runs with that as its working directory.
    if err := godotenv.Load(); err != nil {
        log.Println("no .env file, using process environment")
    }

    dbURL := os.Getenv("DATABASE_URL")
    _ = dbURL
}
```

```bash theme={null}
go get github.com/joho/godotenv
```

### Static

Static sites don't run server-side code. For JavaScript apps that need configuration, use a `config.js` file:

```javascript theme={null}
// config.js (served as a static file)
window.APP_CONFIG = {
  API_URL: 'https://api.example.com',
  ENV: 'production'
};
```

## Common Pattern: Database Credentials

After installing a database service, get the credentials and set them as env vars:

```bash theme={null}
# Install MySQL
instapods services add my-app -s mysql -w

# Get the auto-generated password
instapods services creds my-app -s mysql

# Set them (this writes .env and restarts the app)
instapods env set my-app \
  DB_HOST=localhost \
  DB_PORT=3306 \
  DB_NAME=instapod \
  DB_USER=instapod \
  DB_PASS='<password from creds command>'
```

## Variables InstaPods sets itself

Four keys belong to the platform, and a value you set for them is not applied:

| Key | Why |
| - | - |
| `PORT`, `HOST`, `HOSTNAME` | InstaPods sets these so the proxy can reach your app. An app that binds somewhere else stops serving traffic |
| `NODE_ENV` | Already set to `production` in the app's service unit |

The write still goes through - the file is yours - but `instapods env set` and the dashboard both
warn you at the moment you save, rather than leaving you to discover it as a 502 later:

```bash theme={null}
$ instapods env set my-app PORT=8080
Set 1 variable(s) in /home/instapod/app/.env on my-app (app restarted)
  * PORT

! PORT is set by InstaPods so the proxy can reach your app; we do not apply a value
  set here, and an app that reads it from the file itself will stop serving traffic
```

Over the API, the same text comes back in a `warnings` array on the response.

<Tip>
  Read the port from the environment with a fallback (`process.env.PORT || 3000`) and let InstaPods
  supply it. If your app must listen somewhere specific, change the port your app is *proxied* on
  instead - the Deploy Doctor's [wrong port](/support/troubleshooting#deploy-doctor) fix does exactly that.
</Tip>

## File permissions

`.env` holds secrets, so it is not readable by other accounts on the pod: InstaPods keeps it at mode
`600`, owned by `instapod`. On a PHP pod it is `640` and group `www-data`, because php-fpm reads the
file as that group.

If you create the file yourself over SSH, set the mode yourself - `chmod 600 .env`. InstaPods
re-applies the safe mode on every `env set`, so the simplest way to fix a file you wrote by hand is
to set one variable through the CLI.

## `.env` is not reachable from the web

On the static and PHP presets, `.env` sits inside the directory the web server hands to visitors -
so on those presets the file's own permissions are not the only thing standing between your
database password and the internet.

Every pod's public URL now refuses **any** path containing a dot-file segment. A request for
`/.env`, `/.env.production`, `/.git/config` or `/.npmrc` gets a `403`, whatever preset the pod runs
and whatever is actually on disk. `.well-known/` is deliberately exempt, so ACME certificate
renewal, `security.txt` and app-association files keep working.

```bash theme={null}
curl -o /dev/null -w '%{http_code}\n' https://my-app.nbg1-1.instapods.app/.env
# 403
```

<Warning>
  This is a backstop, not a licence to put secrets in the docroot. It only covers dot-files - a
  `config.php`, a `backup.sql` or a `credentials.json` in the served directory is still a public
  file. Keep anything sensitive outside the web root, or behind your app.
</Warning>

<Note>
  The rule is applied when a pod's public route is written, which happens on start, restart, IP
  change and when a custom domain is added. If `/.env` still answers on a long-running pod,
  restarting it applies the rule.
</Note>

## Updating Variables

`instapods env set` merges: the keys you pass are updated or added, everything else in the file is
left alone, and comments and blank lines survive.

```bash theme={null}
instapods env set my-app DATABASE_URL=postgres://new-credentials@localhost/db DEBUG=false
instapods env unset my-app OLD_KEY
```

Both restart the app service for you.

If you edited `/home/instapod/app/.env` by hand instead - over SSH, in the Web IDE, or with
`instapods exec` - nothing restarts your app, so run `instapods pods reload my-app` yourself. See
[Deploy, Reload, Restart](/guides/operations).

A hand edit also switches off the copy we put in the process environment for that pod, because we
cannot tell what you changed. Your app has to read `.env` itself until your next `instapods env set`
or dashboard save, which regenerates it. While it is off, `instapods env list` says so.

### When the values are not reaching your app

`instapods env list` (and the Environment tab in the dashboard, and `GET /api/pods/{name}/env`)
checks the pod before listing anything, and prints a warning first when the variables in `.env`
are not reaching your app's process. There are two:

| Warning starts with | What happened | What to do |
| - | - | - |
| `Environment injection is off for this pod` | `.env` was edited outside the dashboard, so the generated copy was dropped rather than served stale | Save any variable with `instapods env set` (or the dashboard). That regenerates the copy and turns injection back on |
| `This pod's <unit> does not read the variables we inject` | The pod's app service is not wired to read the generated copy - usually a pod created before this was added | The same: save any variable. The save repairs the wiring on the spot |

Either way the values are still in `/home/instapod/app/.env`, so an app that loads the file itself
keeps working throughout. A pod with no app process (a `static` pod) and a fresh pod with no
variables yet never warn.

<Warning>
  The `.env` file is stored inside your pod. If you delete the pod, the file is lost. Keep a copy of your environment variables in a secure location.
</Warning>

Multi-line values - a private key, a service-account JSON - are supported. Quote the value in your
shell and `instapods env set` stores it whole, newlines and all, so it reads back exactly as you
typed it:

```bash theme={null}
instapods env set my-app FIREBASE_KEY="$(cat service-account.json)"
```

## Best Practices

* **Never commit secrets** to git — always use `.env` files or environment variables
* **Use defaults in code** — Always provide fallback values (`process.env.PORT || 3000`)
* **Load the file anyway** — Your app gets the variables from the process environment without any
  code, but a `load_dotenv()` / `dotenv.config()` also covers the three cases above where we
  deliberately leave a variable out of the generated copy. It costs one line and never hurts
* **Prefer `instapods env set` over editing the file** — it restarts the app for you and quotes
  values that contain `#`, quotes or newlines so they read back exactly as typed
* **Separate configs** — Use different `.env` files for development vs production


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.