# How to Store Docker Secrets and Environment Variables Safely

> How to store secrets and environment variables in Docker safely: why ENV and ARG leak, then env_file, secret mounts, GitHub Actions and key rotation.

URL: https://www.matthewswong.com/en/blog/cara-simpan-secret-environment-variable-docker-aman/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2026-10-17
- Updated: 2026-10-17
- Category: DevOps

> **Key takeaway:** How to store secrets and environment variables in Docker safely: never use ENV or ARG, because both end up in the image. Keep them in a .env file ignored by Git and .dockerignore, pass them at runtime with --env-file or env_file, use a secret mount for build credentials, and rotate any committed key.

Your app works on your laptop because a .env file holds the API key. Then you want to run it in a container and hit the question almost every beginner meets: where does the key go? The shortcut is obvious, writing ENV API_KEY=... in the Dockerfile. That is the route that ships the key to the registry together with the image, readable by anyone who can pull it.

This guide compares four places for a value: ARG, ENV, a runtime environment variable and a secret mount, then carries the idea through GitHub Actions and what to do when a key has already reached Git. Every command is matched against the official Docker and GitHub docs, and the deploy pattern is the one this site uses: secrets live in GitHub, get written to an owner-only .env file on the server, and are read by the container when it starts. Every value in the lab is made up.

Want the concept first, with an animated walkthrough? Read our free lesson:

Docker env vars and secrets: build time vs runtime

. This article is the hands-on side: you type the commands and see for yourself where a value leaks and where it stays safe.

## What you need

Not much. Every step runs on Linux, macOS or Windows as long as Docker is running.

- Docker installed and running (Docker Desktop on macOS or Windows, Docker Engine on Linux).
- A terminal and a text editor. Docker Compose is used in Step 4; current Docker installs include it.
- About 30 minutes and an empty folder called secret-lab.

Use made-up values such as not-a-real-secret. Never paste a real key into a practice lab, least of all into a command that lands in your terminal history.

## Step 1: See for yourself why ARG and ENV are not for secrets

Start with ARG. The Dockerfile reference says an ARG value is not embedded in the image or available in the final container, yet build arguments are visible in docker history, and Docker does not recommend them for secrets. Prove it yourself:

Now ENV. The docs say ENV values are always persisted in the built image and carry into every container run from it. No docker history needed; just run the container:

> If a key ever went into an ARG or ENV and the image was pushed, treat that key as leaked. Rotate it at the provider; deleting the image does not recall copies someone already pulled.

In short, ENV and ARG suit values anyone may read, such as NODE_ENV or a port number. This site's Dockerfile contains only ENV lines like that and declares no ARG at all.

## Step 2: Keep the .env file out of Git and out of the image

Two files stop two different leaks. .gitignore keeps .env out of the repository. .dockerignore keeps it out of the build context, and Docker states that files matching its patterns are removed from the build context before it is sent to the builder, so they cannot be copied into the image with ADD, COPY or bind mounts. This site's repo lists .env and .env.* in both files.

Note that .gitignore only helps for files that were never committed. If .env already reached Git, adding it to .gitignore now does not erase its history; that is Step 7.

## Step 3: Pass the secret at runtime with --env-file or -e

The right pattern: build the image with no secrets, then hand the values to the container when it runs. With --env-file, each line is VAR=value and lines starting with # are comments. With -e VAR and no equals sign, the Docker CLI reads VAR from your shell environment.

One small trap: the docs state that a # anywhere else on a line is part of the value, not a comment. A trailing comment therefore becomes part of your secret and makes authentication fail without a clear message.

> Use -e VAR with no value when you do not want to type a key into the command line. If the variable is not exported in your shell, the docs say it is unset in the container.

## Step 4: Move it into Docker Compose with env_file

On a server you will almost certainly use Compose. The env_file attribute loads a .env file into the container's environment, with its path relative to the location of compose.yaml. Once the file exists, restrict it with chmod 600 so only the owner can read it, as this site's deploy does.

When the same name is set in two places, Compose follows a fixed order. Highest first: docker compose run -e, then environment or env_file with an interpolated value, then environment alone, then env_file alone, and last an ENV in the image. That is why the environment entry beats env_file in the example above.

An honest note: the Compose docs advise against passing passwords through environment variables and offer Compose secrets instead. env_file remains reasonable for one container on one server with an owner-only file, as on this site. For a larger setup, study that secrets option.

## Step 5: Use a secret mount when the build itself needs a credential

Sometimes the build needs a credential, for example to download a private package. Docker writes that build arguments and environment variables are inappropriate here because they persist in the final image, and recommends secret mounts. The value appears as a file at /run/secrets/ followed by its id, only during that one RUN instruction.

This site does not need one, so this part comes purely from the Docker docs. For secrets the running app needs, keep using Steps 3 and 4.

## Step 6: Carry the secret from GitHub Actions to the server

Store the value in GitHub as a secret and read it through the secrets context. The GitHub docs recommend passing a secret to a command as an environment variable referenced in quotes, and warn against secrets on the command line because other users may see them with ps.

This site's deploy writes the .env file on the server over SSH with a quoted heredoc, then runs chmod 600. The shape is below, with names replaced by placeholders:

Do not rely on log redaction. GitHub does redact printed secrets, but its docs state this is not guaranteed because a value can be transformed, and the runner can only redact secrets used in the current job. Never print a secret with echo.

## Step 7: If a secret was already committed or pushed

Order matters. The GitHub docs say the first step is to revoke or rotate the secret; once rotated, the old key can no longer be used, and rewriting history may not be necessary. If you still want to clean history, GitHub points to git-filter-repo version 2.47 or later with the --sensitive-data-removal option:

> Rewriting history and force-pushing is not enough. GitHub warns the old commits may still be reachable in other clones and forks, through cached views by SHA-1 hash, and through pull requests that reference them. That is why rotating the key is always the first step.

After that, go back to Step 2 so it does not happen again. See the supporting article at the end for how secret scanning works.

## Common errors and how to fix them

The six symptoms that most often confuse beginners, with their cause and fix.

| Symptom | Cause | Fix |
| --- | --- | --- |
| The key shows in docker history output | The value was passed through ARG or build-arg | Rotate the key, then move to RUN --mount=type=secret |
| printenv in the container is empty although .env has it | Forgot --env-file or env_file, or used -e VAR that is not exported in the shell | Add --env-file .env or env_file: .env, or export the variable first |
| Authentication fails although the key is right | A trailing comment on a .env line became part of the value | Move the comment to its own line starting with # |
| The environment value is used instead of the .env one | In Compose precedence, environment ranks above env_file | Remove the duplicate entry, or use environment on purpose as the override |
| Compose starts with a blank value and no warning | The variable is unset, and plain interpolation uses an empty string | Use the required form with a question mark and an error message, so Compose stops with an error |
| A secret still appears in the GitHub Actions log | Redaction is not guaranteed for transformed values and only covers secrets used in that job | Never print a secret; rotate any key that was printed |

Every symptom above can be reproduced in this lab with made-up values, so try getting it wrong on purpose to learn what it looks like.

## Checklist before you deploy

.env and .env.* are listed in .gitignore and .dockerignore.

The Dockerfile holds only non-sensitive ENV lines and no ARG for secrets.

App secrets are handed over at runtime through env_file or --env-file.

The .env file on the server is restricted with chmod 600.

GitHub Actions secrets are referenced through env with quotes, never written on the command line or printed.

You know the order if a key leaks: rotate first, clean history second.

## Go deeper

For the big picture of where a secret lives at each stage of a deploy, follow the lesson

, complete with a quiz and an animation. For good Dockerfile habits in general, read

. And if you want to step up to Docker secrets in production, continue with

.

The rule to take home: build the image with no secrets, let secrets arrive at runtime, and treat any key someone else has seen as leaked and rotate it. The rest is picking the right place for each value.

## Sources and references:

- Docker docs: Dockerfile reference (ARG and ENV)
- Docker docs: Build secrets
- Docker docs: docker container run (-e and --env-file)
- Docker docs: Environment variable precedence in Compose
- GitHub docs: Using secrets in GitHub Actions
- GitHub docs: Removing sensitive data from a repository

## Source links

- https://docs.docker.com/reference/dockerfile/
- https://docs.docker.com/build/building/secrets/
- https://docs.docker.com/reference/cli/docker/container/run/
- https://docs.docker.com/compose/how-tos/environment-variables/envvars-precedence/
- https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets
- https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository

## FAQ

### Is it safe to put an API key in a Dockerfile ENV?

No. The Docker docs say ENV values are always persisted in the built image and carry into every container run from it, so anyone who can pull the image can read the key. Pass it at runtime with --env-file or env_file instead, and rotate any key that was already baked in.

### Why does my build argument still show up in docker history?

An ARG is not embedded in the final image, but Docker states that build arguments are visible in docker history. That is why Docker does not recommend them for secrets. Use RUN --mount=type=secret when the build needs a credential.

### Does .dockerignore stop my .env file from entering the image?

Yes. Files matching .dockerignore patterns are removed from the build context before it is sent to the builder, and the docs say you cannot copy them into the image with ADD, COPY or bind mounts. Still pass the values at runtime, because the image must not need the file.

### What should I do if I committed a secret to GitHub?

Revoke or rotate the secret first. That alone may be enough because the old key stops working. If you also rewrite history with git-filter-repo and force push, remember GitHub warns the commits may still be reachable through forks, clones, cached views and pull requests.

### Which wins in Docker Compose, environment or env_file?

The environment attribute wins over env_file when neither uses an interpolated value. A value set with docker compose run -e beats both, and an ENV baked into the image has the lowest priority.

---

- HTML version: https://www.matthewswong.com/en/blog/cara-simpan-secret-environment-variable-docker-aman/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/cara-simpan-secret-environment-variable-docker-aman/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
