Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

cp .env.example .env: What That Line Is Doing and What Comes Next

Sean

Platform Writer

Aug 17, 2026
8 min read

.env.example is a committed template listing every environment variable the project needs, with the values blanked out. .env is your local copy with the real values filled in, and it is deliberately excluded from version control. The copy command creates the second from the first, and then you fill in the blanks.

cp .env.example .env: What That Line Is Doing and What Comes Next

The split exists for one reason: the list of settings a project needs is useful to everyone, and the values are secrets. Committing the template documents the contract; committing the real file leaks database passwords and API keys into the repository history, where they remain long after someone notices and deletes the line.

Table of contents

The command, and what it does not do

cp .env.example .env

# or, depending on the project's naming
cp example.env .env
cp .env.sample .env
cp .env.dist .env

There is no standard name. .env.example is the most common, but projects use .env.sample, .env.dist, .env.template, and example.env. Look at what the repository actually contains rather than assuming.

The copy does nothing clever. It does not detect your database, generate keys, or validate anything. It gives you a file with the right keys and empty or placeholder values, which you now have to fill in — usually by reading the README, and occasionally by reading the code that consumes each variable.

On Windows PowerShell the equivalent is Copy-Item .env.example .env, and in cmd it is copy .env.example .env. Setup instructions written for Linux frequently trip people here.

One warning, since it is easy to do and unpleasant to undo: run this in the project directory. Running it from the wrong working directory, or getting the argument order backwards, can overwrite an existing .env you had already filled in — and since it is not in version control, there is no history to recover it from.

Why the file is gitignored

Because it contains credentials. Every project template ships with .env in .gitignore for that reason, and the rule holds regardless of how private you think the repository is.

Two things make a committed .env worse than it first appears:

  • Git history is permanent. Deleting the file in a later commit does not remove it — anyone who can clone the repository can read the earlier commit. Removing it properly means rewriting history and force-pushing, and coordinating that across everyone who has cloned.
  • Repositories move. A private repository becomes public, gets forked, or a contractor keeps a clone. The credential outlives every assumption you made about who could see it.

If a secret does get committed, the only reliable response is to rotate it. Treat it as public from that moment, change the credential at the source, and then clean the history if you want to — in that order, because the cleanup takes time and the rotation does not.

Verify the ignore rule is actually working before you fill anything in:

git check-ignore -v .env
git status --porcelain | grep -F '.env'

The first prints the ignore rule matching the file. Silence from it means the file is not ignored, which is worth knowing before rather than after.

Keeping the example file honest

.env.example is documentation, and like all documentation it drifts. Someone adds a variable, sets it locally, ships it, and never updates the template. The next person clones the repository and hits a runtime error about a missing key with no indication what value it wants.

A good example file carries structure and comments:

# --- Application ---
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:3000

# --- Database ---
# Full connection URL. Local default assumes Postgres on 5432.
DATABASE_URL=postgresql://user:password@localhost:5432/appdb

# --- Third-party ---
# Get a test key from the provider dashboard. Test keys only in dev.
STRIPE_SECRET_KEY=

# --- Optional ---
# Leave blank to disable error reporting locally.
SENTRY_DSN=

Placeholder values for anything with a sensible local default, empty for anything that must be supplied, and a comment saying where to get it. That turns setup from a scavenger hunt into filling in five blanks.

Better still, validate at startup. A check that fails immediately with a clear list of missing variables beats a null-reference error three layers into a request handler.

const required = ['DATABASE_URL', 'SESSION_SECRET', 'STRIPE_SECRET_KEY'];
const missing = required.filter((k) => !process.env[k]);
if (missing.length) {
  throw new Error(`Missing environment variables: ${missing.join(', ')}`);
}

Libraries exist that do this with schema validation and type coercion, which is worth adopting on anything with more than a handful of variables.

Multiple environments

Most frameworks support a layered set of files, and the loading order is the part that produces confusing bugs.

  • .env — base values, gitignored.
  • .env.local — machine-specific overrides, gitignored. Usually loaded last, so it wins.
  • .env.development / .env.production — per-environment values, sometimes committed if they contain no secrets.
  • .env.test — test-specific settings, usually committed.

The precedence differs between frameworks, and assuming one framework’s order applies to another is a reliable way to spend an hour on a value that is being silently overridden. Read your framework’s documentation for the exact order rather than guessing.

One rule that holds everywhere: an actual environment variable set in the shell usually beats anything in a file. That is deliberate, and it is what makes container and CI deployment work without any .env file at all.

DATABASE_URL=postgres://... npm start
printenv DATABASE_URL

Production does not use a .env file

This is the part the local workflow does not teach, and it matters.

In production, environment variables should come from the platform — the container runtime, the orchestrator, the hosting provider’s configuration — not from a file sitting on disk next to your code. The reasons are practical rather than ideological:

  • A file on disk is readable by anything that can read the filesystem, including a path traversal bug or a misconfigured static file server. There are well-known incidents of .env files served over HTTP because they sat inside a web root.
  • Rotation should not require a deploy. Changing a variable in a platform is a restart; changing a file baked into an image is a rebuild.
  • Secrets in an image leak sideways — into registries, into layer caches, into anyone who can pull the image.
  • Per-environment values need to differ without maintaining parallel files that drift.

The right shape is that your application reads process.env (or its language equivalent) and does not care where the values came from. Locally a dotenv loader populates it from a file; in production the platform populates it directly. Same code, different source, no branching on environment.

If you ever must have a .env on a server, keep it outside the web root and restrict its permissions to the application user.

A short setup checklist

  1. cp .env.example .env in the project directory.
  2. Confirm .env is gitignored with git check-ignore -v .env.
  3. Fill in every blank value; read the comments for where to obtain each one.
  4. Use test or sandbox credentials locally. Never production keys on a laptop.
  5. Start the app and confirm the startup validation passes.
  6. When you add a variable, add it to .env.example in the same commit.

That last one is the habit that keeps the whole system working. The template is only useful if it is accurate, and the only moment anyone reliably remembers to update it is the moment they add the variable.

How this fits the rest of the stack

The production half of this — variables supplied by the platform rather than a file — is exactly what removes the most common way secrets escape. On RunxBuild, environment variables are set per service in the dashboard, injected at runtime, and changed without a rebuild, so rotating a key is a restart rather than a new image. The application reads process.env the same way it does locally and never needs to know the difference. Services on RunxBuild covers environment variables alongside deploys, logs, and rollback, and when you are working out what a project costs with separate staging and production configurations, the RunxBuild hosting calculator shows the service, database, storage, and bandwidth as individual lines.

Useful related references:

FAQ

What does cp .env.example .env do?

It copies the committed template of required environment variables into the file your application actually reads. The template documents which settings the project needs with values blanked out; your copy holds the real values and is excluded from version control.

Why is .env in gitignore?

Because it contains credentials. Git history is permanent, so committing it once means anyone who can clone the repository can read the secret from an earlier commit even after you delete the file. If a secret does get committed, rotate it immediately — cleaning history comes second.

What do I put in the .env file?

Whatever the example file lists: database connection strings, API keys, session secrets, and service URLs. Use test or sandbox credentials locally, never production keys. Read the comments in the example file for where to obtain each value.

Should I use a .env file in production?

No. Production values should come from the platform’s environment variable configuration, not a file on disk. A file can be read by a path traversal bug or served accidentally by a misconfigured web server, and rotating a value in a file requires a rebuild rather than a restart.

Why is my environment variable not being picked up?

Check the loading order. Frameworks layer .env, .env.local, and per-environment files with different precedence, and a real shell environment variable usually overrides all of them. Print the resolved value at startup rather than assuming which file won.

#cp .env.example .env#environment variables#dotenv#12 factor#configuration