Published 2026-10-22 · Reviewed 2026-10-22
A .env file is not secret management
A .env file keeps a value beside the process. Secret management still has to answer where that value lives, who can read it, how it is replaced, and how the backup is protected.
- home servers
- secrets
- configuration
The file next to the Compose file
Take an invented example, not a real server and not a real password. A home server starts one container from a Compose file. A `.env` beside it holds a database password as a name and a value.
OWASP's Secrets Management Cheat Sheet says many organizations leave API keys, database credentials, SSH keys and certificates in plaintext in source and configuration. It describes centralizing storage, provisioning, auditing, rotation and management. The file is the configuration. Those other jobs are secret management.
Four questions sort the design: where the value is stored, who can read it, how a leak is replaced, and how you recover it without a second uncontrolled copy. A text file answers the first. Interpretation: loading it into the environment moves those bytes somewhere more programs can see.
Plaintext, then the environment
systemd's `EnvironmentFile=` reads newline-separated assignments from a text file just before start, from the service manager's filesystem, before that service's bind mounts. They become environment variables and override `Environment=`. The manual says variables are unsuitable for secrets such as passwords or key material: values set for a unit are exposed to unprivileged clients via D-Bus, and they propagate down the process tree, including across setuid and setgid executables.
Docker's Compose guide says injecting passwords and API keys as environment variables risks unintentional exposure. The variables are often available to every process, access is hard to track, and debugging can print them into logs. OWASP's container section adds system dumps, prefers a mounted file or a secret store when either is possible, and says secrets hardcoded with `ENV` or `ARG` leak with the container definition.
Docker's commit documentation runs `docker inspect -f "{{ .Config.Env }}"` and prints a container's environment: in the example, `HOME` and `PATH`. After a commit adds `ENV DEBUG=true`, the new image's list includes `DEBUG=true`. That example is not a password.
Keeping the file out of git, and closed on disk
Git's gitignore manual says `.gitignore` names intentionally untracked files, and that tracked files are unaffected. `git rm --cached` removes a tracked file from the index. Judgment: that changes the next commit. The manual does not describe rewriting older commits, so a password committed earlier remains in that history.
Gitleaks, on the README checked on 22 October 2026, detects secrets in git repositories. Its hook example pins v8.24.2 and says to run `pre-commit autoupdate`. The hook command is `gitleaks git --pre-commit --redact --staged`: staged changes, with output redacted. History is `gitleaks git`, which reads patches with `git log -p`. `SKIP=gitleaks` skips the hook, and `gitleaks:allow` ignores a line. The README says the project is feature complete, later releases are security patches only, and the author is moving to Betterleaks. Judgment: run the hook, then scan history. Skipping it is not an access record.
OWASP's Docker Security Cheat Sheet says file-backed Compose secrets are bind mounts. Protect the source files with permissions and storage encryption: declaring the secret does not encrypt them. `systemd-creds` calls any mode other than `0400` insecure, meaning readable by anyone but the owner. Judgment: owner-readable only is the bar for the host file. The mode neither encrypts it nor expires a copy that has left the disk.
A mount, a swarm store, or a credential file
Compose grants a secret only when a service's `secrets` attribute names it. On Linux the value is a file under `/run/secrets/`, from a single-file bind mount. Windows containers are outside that path: they bind-mount directories only. Long syntax defaults to mode `0444`, world-readable. `uid`, `gid` and `mode` apply only for an `environment` source. A file source is a bind mount, and Compose silently ignores those fields. The other source is a host environment variable. Compose supports it. `docker stack deploy` does not.
Swarm secrets are for swarm services, not standalone containers. Docker encrypts them in transit and at rest, sends them over mutual TLS, and stores them in the encrypted Raft log. The decrypted file lives in memory and is flushed when the task stops. The WordPress example mounts them on a tmpfs, keeps them out of the environment, and shows that `docker commit` does not capture them. A secret still in use cannot be removed. Put a version or a date in the name, then update the service to grant or revoke access. A one-machine Compose file is the bind mount above, not this store.
`LoadCredential=` and `LoadCredentialEncrypted=` pass a file under `$CREDENTIALS_DIRECTORY`. It is read-only, in unswappable memory when permitted, and open to the unit's user and the superuser. The manager reads the source and gives the unit a copy, including under `DynamicUser=`, up to 1 MB. Encrypted credentials decrypt at start from the TPM2, `/var/lib/systemd/credential.secret`, or both. The manual says they cannot easily be analyzed offline or generated externally, and the service still receives plaintext. `SetCredential=` is a literal unprivileged clients can read over IPC, so it is for user ids and public keys. Judgment: nothing here records who read the value. The host file still needs a mode and a backup.
Replace the value, and protect the copy you keep
OWASP's lifecycle is creation, rotation, revocation and expiration. Rotate so a stolen credential works only briefly. The page puts the lifetime from minutes to years, depending on what the secret protects. User credentials are the exception: rotate them on suspicion or evidence of compromise, which the cheat sheet attributes to NIST. Revoke a secret that is finished or may have leaked. A restart does not itself revoke a dynamic credential; it lasts until the backing service expires or revokes it.
Judgment, for the invented file: a new line and a restart update that process. An old commit, a debug log and a backup each stay a password until that copy is destroyed.
Backups of this material, on the same page, are automated, restore-tested, encrypted, stored with reduced access, and watched. Emergency credentials are backed up in a second store and tested. Interpretation: a plaintext `.env` in an unencrypted disk image is another plaintext copy. A backup is not a restore plan until the restore has been done, and a restore into a looser place discloses the secret.
When several consumers share one secret, that page's store is fine-grained access, an audit of who asked and whether it was approved, and metadata for creation, use and rotation. Centralizing can mean more than one tool, and the store's own primary credential lives somewhere else. One home service can stay with a permissioned file and a per-service mount. That still produces no audit row.
This article was drafted with AI assistance and checked against the cited pages on 22 October 2026. It reports no measured leak and no testimonial.
What we would do next
On the invented server, leave the password file untracked and scan history once. Make the host file owner-readable only. Pass a path, with a Compose file secret or `LoadCredential=`, and keep `SetCredential=` for values you would show someone. Write the leak steps before you need them: the new value, which processes restart, and which backups and commits still hold the old one. Encrypt that backup, and restore it once into a directory you can delete.
Sources and limits
This article synthesises the sources below into a practical explanation. It is not a security standard, legal advice, or a guarantee that guidance current at review time still applies — check the review date above against your own situation.
- Secrets Management Cheat Sheet — OWASP Cheat Sheet Series. Describes plaintext secrets in source and configuration, and the jobs of storage, auditing, rotation, revocation, backup and least-privilege access. Says container environment variables are a poor default, and that restarting an application does not itself revoke a dynamic credential.
- Docker Security Cheat Sheet — OWASP Cheat Sheet Series. Says file-backed Compose secrets are bind mounts, that declaring one does not encrypt the file, and that the host file needs permissions and storage encryption. Contrasts that with Swarm secrets sent over mutual TLS and stored in an encrypted Raft log.
- Manage secrets securely in Docker Compose — Docker. Says passwords and API keys injected as environment variables risk unintentional exposure, can be visible to processes and printed in logs, and that Compose secrets are per-service files under /run/secrets/ on Linux, delivered by a single-file bind mount.
- Define services in Docker Compose — Docker. Documents the per-service secrets grant, the long-syntax default mode 0444, and that uid, gid and mode are ignored when the secret comes from a file.
- Secrets top-level element — Docker. Says a Compose secret comes from a file or from a host environment variable, and that the environment source is not supported by docker stack deploy.
- Manage sensitive data with Docker secrets — Docker. Describes Swarm secrets as encrypted in transit and at rest, mounted in memory for running tasks only, unavailable to standalone containers, and not exposed as environment variables in the WordPress example.
- docker container commit — Docker. Shows docker inspect printing .Config.Env for a container, and shows ENV DEBUG=true appearing in that list on the committed image.
- systemd.exec — systemd. Says environment variables are unsuitable for secrets, documents EnvironmentFile= and LoadCredential=/LoadCredentialEncrypted=, and says SetCredential= is exposed over IPC.
- systemd-creds — systemd. Labels a credential insecure when its mode is anything other than 0400, meaning readable by anyone but the owner.
- gitignore — Git. Says a gitignore file names intentionally untracked files, that tracked files are unaffected, and that git rm --cached removes a tracked file from the index.
- README — Gitleaks. Documents a pre-commit hook, a history scan via git log -p, the SKIP=gitleaks bypass, gitleaks:allow, and the project's feature-complete warning.
- Pre-commit hook definition — Gitleaks. Defines the gitleaks hook as gitleaks git --pre-commit --redact --staged.
More from Server Attic
A restart policy is not supervision
A restart policy brings a container back after it exits. Supervision is the separate work of noticing the repeats, limiting them, and escalating while the process is still failing.
A UPS buys minutes, not a clean shutdown
A UPS keeps the power supply alive for a while. The server still has to hear about the outage, shut down inside the runtime it actually has, and know what "power is back" is supposed to look like.
A passed SMART test is not a healthy disk
A drive that reports PASSED has cleared one vendor check. Record a baseline, schedule self-tests, watch a few raw counters for change over time, and have a restore you trust before you act on any of it.

Community comments
Comments are reviewed before publication. Keep discussion constructive: no harassment, hate, threats, doxxing, spam, illegal material, or attempts to evade moderation.
No approved comments yet.
Sign in to join the discussion.