---
title: "PM2 Environment Variables: Setup, Update & Best Practices"
description: "Master PM2 environment variables: learn to set, update, and secure them using ecosystem files, --update-env, filter_env, and more. Avoid common pitfalls."
date: 2026-07-17
categories: ["tools"]
tags: ["pm2","node","environment-variables"]
---

import Notice from "@components/widgets/Notice.astro";
import ListCheck from "@components/widgets/ListCheck.astro";
import Accordion from "@components/widgets/Accordion.astro";
import Tabs from "@components/widgets/Tabs.astro";
import Tab from "@components/widgets/Tab.astro";

PM2 environment variables are one of those things that seem simple until they bite you. You set a variable, restart the process, and nothing changes. Or you use the wrong flag and wonder why your app is running on the wrong port. I've seen these mistakes trip up experienced developers, and the old PM2 documentation doesn't help much.

This guide covers the correct way to set, update, and secure environment variables in PM2 (currently at version 7.0.3, requiring Node.js 18+). If you're looking for a broader overview of process management, check [managing applications with PM2](https://www.bitdoze.com/pm2-manage-apps/) first. For operators running multiple apps on a VPS, the [best self-hosted server management panels](https://www.bitdoze.com/best-self-hosted-panels/) can help with visibility across your stack.

<Notice type="error" title="Common Misconception">
The `--env` flag in PM2 does **not** set individual environment variables. It selects named environment blocks from an ecosystem file (e.g., `--env production`). Many articles, including older versions of this one, show incorrect syntax like `pm2 start app.js --env PORT=3000`. That doesn't work.
</Notice>

## Setting environment variables via the command line in PM2

The correct way to pass environment variables when starting a PM2 process uses [standard Unix environment variable syntax](https://www.bitdoze.com/linux-commands/): prefix the command with `VAR=value`.

<Tabs>
<Tab name="Wrong way (does not work)">
```sh
# ❌ This is NOT how PM2 env vars work
pm2 start app.js --env PORT=3000
pm2 start app.js --env PORT=3000,DB_URL=mongodb://localhost:27017/mydb
```
The `--env` flag selects environment **blocks** from an ecosystem file, not individual variables. Passing `--env PORT=3000` will either be ignored or cause an error.
</Tab>
<Tab name="Correct way (Unix prepend)">
```sh
# Single variable:
PORT=3000 pm2 start app.js

# Multiple variables (space-separated):
PORT=3000 DB_URL=mongodb://localhost:27017/mydb SECRET=abc123 pm2 start app.js
```
This is standard Unix. The variables are set in the environment before PM2 spawns your application.
</Tab>
</Tabs>

**Verify it worked:**

```sh
# Find your process ID first
pm2 list

# Then inspect the environment (replace 0 with your process id)
pm2 env 0
```

You should see `PORT=3000` in the output. If it's missing, double-check the prepend syntax. The variables must come *before* `pm2 start`.

For [PM2 cluster mode](https://www.bitdoze.com/pm2-fork-cluster/) with multiple instances, you'll typically want different ports per instance. `increment_var` handles that automatically (see the advanced section).

## Setting environment variables with PM2 ecosystem files

For any production application, the ecosystem file is the right approach. It gives you a single config file that defines your app, its environment variables, and how PM2 should run it.

### Basic env, env_production, and env_development blocks

```js
// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: "my-app",
      script: "./app.js",
      // Default env. Used when no --env flag is passed
      env: {
        NODE_ENV: "development",
        PORT: 3000,
        APP_LOG_LEVEL: "debug",
      },
      // Used with: pm2 start ecosystem.config.js --env production
      env_production: {
        NODE_ENV: "production",
        PORT: 3000,
        APP_LOG_LEVEL: "info",
      },
      // Used with: pm2 start ecosystem.config.js --env staging
      env_staging: {
        NODE_ENV: "staging",
        PORT: 3000,
        APP_LOG_LEVEL: "warn",
      },
    },
  ],
};
```

Each `env_*` block corresponds to a name you pass with `--env`. The base `env` block is the default when no `--env` flag is used.

### Switching environments with --env [name]

```sh
# Start with development env (default):
pm2 start ecosystem.config.js

# Start with production env:
pm2 start ecosystem.config.js --env production

# Start with staging env:
pm2 start ecosystem.config.js --env staging
```

<Notice type="info" title="Ecosystem env vars update automatically on restart">
When you use an ecosystem file, PM2 re-reads the file on `pm2 restart`. So if you edit the `env_production` block and run `pm2 restart ecosystem.config.js --env production`, the new values are picked up. This is different from CLI-set variables, which are conservative by default (more on that below).
</Notice>

### Modern ecosystem file formats and scaffolding

PM2 supports more than just `.js` files:

- `ecosystem.config.js` / `ecosystem.config.cjs` / `ecosystem.config.mjs`
- `ecosystem.json` / `ecosystem.json5`
- `ecosystem.yaml` / `ecosystem.yml`

To scaffold a starter config:

```sh
pm2 init simple
```

This creates a basic `ecosystem.config.js` you can fill in.

**Verify:**

```sh
pm2 start ecosystem.config.js --env production
pm2 env 0   # Confirm NODE_ENV=production appears
```

If the wrong env block values appear, the `--env` name doesn't match your ecosystem file key. `--env prod` will **not** match `env_production`. The name must match exactly.

## How to update environment variables for a running PM2 application

This is where most people get stuck. PM2 is conservative by default: environment variables are essentially immutable once a process starts.

<Notice type="warning" title="Restarts are conservative by default">
From the official PM2 docs: "Via CLI, the environment is conservative meaning that, when you will run different process management actions (restart, reload, stop/start), new environment variables will not be updated into your application." You must explicitly opt in with `--update-env`.
</Notice>

### The --update-env flag

To change a variable for a CLI-started process:

```sh
# Set the new value and restart with --update-env
PORT=4000 pm2 restart my-app --update-env
```

To confirm the change took effect:

```sh
pm2 env 0   # Look for PORT=4000
```

For ecosystem file changes, just restart with the same `--env` flag:

```sh
# Edit env_production in ecosystem.config.js, then:
pm2 restart ecosystem.config.js --env production
```

Ecosystem file variables are always updated on restart. You don't need `--update-env` for those.

### Full delete + restart for stubborn variables

Some variables, most notably `NODE_ENV`, are read once at startup and won't change even with `--update-env`. When that happens, delete and re-add is the only reliable path:

```sh
pm2 delete my-app
pm2 start ecosystem.config.js --env production
```

This is a known behavior. Community reports (GitHub issues #3192, #4135, #5591) confirm that `--update-env` can be unreliable in certain edge cases. When in doubt, delete and re-add.

### The pm2 save / pm2 resurrect footgun

If you use `pm2 save` to persist your process list for auto-restart on reboot (via `pm2 startup`), be aware: `pm2 save` freezes the current environment variables into the dump file. When you later run `pm2 resurrect`, those saved values are used, even if you've changed the ecosystem file.

The fix: after changing env vars, delete and re-add your processes, then save again:

```sh
pm2 delete my-app
pm2 start ecosystem.config.js --env production
pm2 save
```

<Notice type="error" title="pm2 set Does NOT Update Application Env Vars">
`pm2 set` is for PM2's **internal configuration system** (e.g., `pm2 set pm2:sysmonit true` to toggle host monitoring). It does not set application environment variables and does not send any signal to your process. If you've been using `pm2 set app:KEY value`, that value is not reaching your application.
</Notice>

## Advanced PM2 environment variable features

These features matter most when running multiple instances or deploying to multiple environments on the same server.

### filter_env: prevent global env var leaks

By default, your PM2 process inherits **all** environment variables from the parent shell. That includes system vars, vars from other apps, and anything exported in your `.bashrc`. `filter_env` lets you whitelist a prefix.

```js
module.exports = {
  apps: [
    {
      name: "my-app",
      script: "./app.js",
      // Only pass env vars that start with APP_ or DB_
      filter_env: ["APP_", "DB_"],
    },
  ],
};
```

To drop all global env vars entirely:

```js
filter_env: true,
```

This is a security feature. It prevents accidental leakage of secrets or variables from other processes. See the security section for more.

### increment_var: auto-increment PORT per cluster instance

When running in [PM2 cluster mode](https://www.bitdoze.com/pm2-fork-cluster/), each instance needs its own port. Instead of hardcoding ports, use `increment_var`:

```js
module.exports = {
  apps: [
    {
      name: "api",
      script: "./api.js",
      instances: 4,
      exec_mode: "cluster",
      increment_var: "PORT",
      env: {
        PORT: 3000,
      },
    },
  ],
};
```

This produces: instance 0 gets `PORT=3000`, instance 1 gets `PORT=3001`, instance 2 gets `PORT=3002`, instance 3 gets `PORT=3003`.

### instance_var: customize NODE_APP_INSTANCE

PM2 sets `NODE_APP_INSTANCE` for each cluster instance (0, 1, 2, ...). This conflicts with the `node-config` library, which uses the same variable for its own purposes. Rename it:

```js
instance_var: "INSTANCE_ID",
```

Now each instance gets `INSTANCE_ID=0`, `INSTANCE_ID=1`, etc. instead of `NODE_APP_INSTANCE`.

### append_env_to_name: multi-environment on one server

If you want to run the same application in development and production on a single machine:

```js
module.exports = {
  apps: [
    {
      name: "my-app",
      script: "./app.js",
      append_env_to_name: true,
      env: { NODE_ENV: "development" },
      env_production: { NODE_ENV: "production" },
    },
  ],
};
```

With `pm2 start ecosystem.config.js --env production`, PM2 names the process `my-app-production`. With no flag, it's `my-app-development`. Both can run simultaneously.

## Listing and inspecting environment variables in PM2

To see all environment variables for a running process:

```sh
# By process ID
pm2 env 0

# By process name
pm2 env my-app
```

This shows every environment variable the process can see, both the ones you set and all the inherited system variables.

For JSON output (useful for scripting):

```sh
pm2 jlist | jq '.[0].pm2_env'
```

For a formatted view of all processes:

```sh
pm2 prettylist
```

<Notice type="warning" title="Secrets Are Visible in pm2 env Output">
Anyone with shell access can run `pm2 env <id>` and see all environment variables, including database passwords, API keys, and tokens. This is another reason to use `filter_env` and to restrict SSH access. For broader guidance on [monitoring your application processes](https://www.bitdoze.com/sever-monitoring/) and server security, see the linked guides.
</Notice>

## Security best practices for PM2 environment variables

<ListCheck>
<ul>
<li>Never hardcode secrets in ecosystem files committed to git</li>
<li>Use `filter_env` to limit which variables your process can see</li>
<li>Inject secrets at deploy time, not at development time</li>
<li>Restrict SSH access. `pm2 env` exposes everything to anyone with a shell</li>
<li>Rotate secrets regularly and restart with `--update-env` or delete+start</li>
<li>Audit your environment periodically with `pm2 env <id>`</li>
</ul>
</ListCheck>

### Never hardcode secrets in ecosystem files

Your `ecosystem.config.js` should be in version control. That means it should never contain real passwords, API keys, or tokens. Instead, inject them at deploy time:

**Option 1: Wrapper script that sources a .env file**

```sh
#!/bin/bash
# start-app.sh
set -a
source /etc/my-app/secrets.env
set +a
pm2 start ecosystem.config.js --env production
```

**Option 2: Systemd environment file**

If PM2 is managed by systemd (via `pm2 startup`), you can set variables in `/etc/default/my-app` and reference them in the unit file.

**Option 3: Secrets manager**

For teams and more complex setups, tools like Infisical, Doppler, or HashiCorp Vault can inject secrets at deploy time. For simple solo setups, a `.env` file with restricted permissions (`chmod 600`) sourced by a wrapper script is usually enough.

For readers managing secrets in containerized environments, see [Docker Compose secrets management](https://www.bitdoze.com/docker-compose-secrets/) for a comparison of approaches.

### Using filter_env to limit exposure

If your server runs multiple PM2 applications, `filter_env` prevents one app from seeing another app's variables. Prefix-based filtering is the most practical:

```js
filter_env: ["MYAPP_"],
```

Now only variables starting with `MYAPP_` reach your process. Everything else is stripped.

### Broader server security

Environment variables are only one part of the picture. If you haven't already, review [securing your SSH server in Linux](https://www.bitdoze.com/secure-ssh-server-linux/). If someone gets shell access, all your env vars are exposed regardless of how well you manage them in PM2.

## Common PM2 environment variable pitfalls (and how to avoid them)

<Accordion label="--env selects environment blocks, not individual variables" group="pitfalls" expanded="true">
`pm2 start app.js --env PORT=3000` does **not** set `PORT`. The `--env` flag selects a named environment block from an ecosystem file. Use `--env production` to use the `env_production` block. To set individual variables on the command line, use the Unix prepend syntax: `PORT=3000 pm2 start app.js`.
</Accordion>

<Accordion label="Restarts don't pick up new env vars" group="pitfalls">
PM2 is conservative by default. Running `pm2 restart my-app` will **not** pick up new environment variables from the shell. You must add `--update-env`: `PORT=4000 pm2 restart my-app --update-env`. For ecosystem file variables, restart with the ecosystem file and `--env` flag. Those are always refreshed.
</Accordion>

<Accordion label="NODE_ENV won't change with --update-env" group="pitfalls">
`NODE_ENV` is read once at application startup. Even `--update-env` won't change it. The only reliable way is the nuclear option: `pm2 delete my-app` followed by `pm2 start ecosystem.config.js --env production` (or the CLI equivalent with the new `NODE_ENV` value).
</Accordion>

<Accordion label="pm2 save freezes env vars into the dump file" group="pitfalls">
When you run `pm2 save`, PM2 dumps the current process list, including all environment variables, to `~/.pm2/dump.pm2`. On `pm2 resurrect` (often triggered automatically by `pm2 startup`), those saved values are used. If you changed env vars in the ecosystem file after saving, the resurrected processes will still use the old values. Delete, re-add, and save again.
</Accordion>

<Accordion label="Secrets are visible in pm2 env output" group="pitfalls">
Any user with shell access can run `pm2 env <id>` and see all environment variables, including database passwords and API keys. Use `filter_env` to limit what your process inherits, and restrict SSH access to the server.
</Accordion>

<Accordion label="pm2 set is for PM2 internal config, not application vars" group="pitfalls">
`pm2 set pm2:sysmonit true` toggles PM2's host monitoring. `pm2 set myapp:PORT 3000` does **not** set an application environment variable. There is no `pm2 set` command that affects `process.env` in your application. Use ecosystem files or CLI prepend + `--update-env` instead.
</Accordion>

## Conclusion

PM2 environment variables come down to a few core rules:

1. **Use ecosystem files for production.** They're explicit, version-controllable, and PM2 re-reads them on restart.
2. **Use `--update-env` when changing CLI-set variables.** Without it, `pm2 restart` is conservative and won't pick up changes.
3. **Delete and re-start for `NODE_ENV` changes.** `--update-env` doesn't work for variables read at startup.
4. **Use `filter_env` to limit exposure.** Don't let your app inherit the entire server environment.
5. **Test with `pm2 env <id>`.** Always verify what your process actually sees.

For the full process management picture, see the [complete guide to managing applications with PM2](https://www.bitdoze.com/pm2-manage-apps/) and [choosing between fork and cluster mode](https://www.bitdoze.com/pm2-fork-cluster/). And if your Node.js dependencies are behind, [keeping your Node.js environment up to date](https://www.bitdoze.com/nodejs-update-dependencies/) is worth the effort. PM2 7.x requires Node.js 18+.

If you're looking for an affordable VPS to run your PM2-managed apps, [Hetzner Cloud](https://go.bitdoze.com/hetzner) offers solid price-to-performance for European and US regions. [Hostinger VPS](https://go.bitdoze.com/hostinger-vps) is another budget option with NVMe storage.