How to Use Docker Secrets and Environment Variables Securely on RHEL 7

One of the most common security mistakes in containerised deployments is embedding sensitive values — database passwords, API keys, TLS certificates — directly inside Dockerfiles or baking them into image layers. Because Docker images are built as a stack of read-only layers, anything written during a RUN step or declared with ENV persists in the image history and is visible to anyone who can run docker history or inspect the image manifest. This tutorial covers the correct approaches to handling secrets and environment variables in Docker on Red Hat Enterprise Linux 7, from simple --env-file usage through Docker Swarm native secrets and BuildKit’s build-time secret injection.

Prerequisites

  • RHEL 7 with Docker CE installed and enabled via systemctl
  • Docker version 18.09 or later for BuildKit support
  • Root or docker group membership
  • Basic understanding of Dockerfiles and docker run
  • For Swarm secrets: a Swarm cluster initialised with docker swarm init

Step 1: Understanding the Leak Risk in Dockerfiles

Never place secrets directly in a Dockerfile using ENV or as arguments to RUN. Even if you later delete the value with another RUN step, it remains visible in the intermediate image layer:

# ❌ INSECURE: secret visible in image history
FROM alpine
ENV DB_PASSWORD=supersecret123
RUN echo "configured"

Anyone with access to the image can retrieve the value:

docker history --no-trunc my-insecure-image
docker inspect my-insecure-image | grep -A5 Env

The same risk applies to passing secrets as --build-arg values in standard (non-BuildKit) builds, since build arguments also appear in docker history.

Step 2: Using –env-file for Runtime Secrets

The simplest improvement is to keep secrets out of the Dockerfile entirely and supply them at runtime via an environment file. Create a file outside of source control:

mkdir -p /etc/myapp-secrets
chmod 700 /etc/myapp-secrets

cat > /etc/myapp-secrets/production.env <<'EOF'
DB_HOST=db.internal.example.com
DB_USER=myapp
DB_PASSWORD=correcthorsebatterystaple
API_KEY=sk-prod-xxxxxxxxxxxxxxxx
EOF

chmod 600 /etc/myapp-secrets/production.env

Run the container referencing this file with --env-file:

docker run -d 
  --env-file /etc/myapp-secrets/production.env 
  --name myapp 
  myapp:latest

The Dockerfile itself references the variable names without values, which is safe to commit to version control:

FROM node:18-alpine
WORKDIR /app
COPY . .
# DB_PASSWORD and API_KEY injected at runtime — never hardcoded here
CMD ["node", "server.js"]

Ensure the .env file is listed in both .gitignore and .dockerignore to prevent accidental inclusion in builds or commits.

Step 3: Initialising Docker Swarm and Creating Swarm Secrets

Docker Swarm provides a first-class secrets subsystem that stores encrypted values in the Swarm Raft log and makes them available to containers only as in-memory tmpfs mounts at /run/secrets/<secret-name>. Secrets are never written to disk on worker nodes.

Initialise a single-node Swarm if you have not already:

docker swarm init

Create secrets from values or files:

# From a string (piped to stdin)
echo -n "correcthorsebatterystaple" | docker secret create db_password -

# From a file
docker secret create api_key /etc/myapp-secrets/api_key.txt

# List secrets (values are never shown)
docker secret ls

Step 4: Using Swarm Secrets in a Docker Stack

Define secrets in a docker-compose.yml (v3.1+) and reference them in service definitions. The secret file appears at /run/secrets/db_password inside the container:

cat > docker-compose.yml <<'EOF'
version: "3.8"

services:
  app:
    image: myapp:latest
    secrets:
      - db_password
      - api_key
    environment:
      - DB_HOST=db.internal.example.com
      - DB_USER=myapp
    deploy:
      replicas: 2

secrets:
  db_password:
    external: true
  api_key:
    external: true
EOF

docker stack deploy -c docker-compose.yml mystack

Inside your application, read the secret from the filesystem rather than an environment variable:

// Node.js example: reading a Swarm secret from /run/secrets/
const fs = require('fs');

function readSecret(name) {
  try {
    return fs.readFileSync(`/run/secrets/${name}`, 'utf8').trim();
  } catch (err) {
    throw new Error(`Secret '${name}' not available: ${err.message}`);
  }
}

const dbPassword = readSecret('db_password');
const apiKey     = readSecret('api_key');

Step 5: Using BuildKit –secret for Build-Time Secrets

Sometimes a build step genuinely needs a secret — for example, running npm install against a private registry, or fetching a private package. Docker BuildKit’s --secret flag mounts the secret into the build step as a tmpfs file that never appears in the resulting layer or image history.

Enable BuildKit and use --mount=type=secret:

# Enable BuildKit (RHEL 7 / Docker CE 18.09+)
export DOCKER_BUILDKIT=1

cat > Dockerfile.buildkit <<'EOF'
# syntax=docker/dockerfile:1
FROM node:18-alpine AS builder
WORKDIR /build
COPY package*.json ./

# The secret is mounted temporarily at /run/secrets/npmrc — not stored in layer
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc 
    npm ci

FROM node:18-alpine
WORKDIR /app
COPY --from=builder /build/node_modules ./node_modules
COPY . .
CMD ["node", "server.js"]
EOF

# Build with the secret mounted from a local file
docker build 
  --secret id=npmrc,src=$HOME/.npmrc 
  -t myapp:private-registry 
  -f Dockerfile.buildkit .

After the build, inspect the history to confirm the secret is absent:

docker history --no-trunc myapp:private-registry | grep npmrc
# Should return nothing

Step 6: HashiCorp Vault Integration Pattern

For production deployments requiring dynamic secrets with short TTLs, HashiCorp Vault is a common solution. The typical pattern uses a Vault agent sidecar or an init container to fetch credentials at startup and write them to a shared volume:

# Install Vault client on RHEL 7
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo 
  https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum install -y vault

# Example: fetch a secret at container start via entrypoint script
cat > entrypoint.sh <<'EOF'
#!/bin/sh
set -e
# Authenticate with Vault using AppRole
VAULT_TOKEN=$(vault write -field=token auth/approle/login 
  role_id="$VAULT_ROLE_ID" 
  secret_id="$VAULT_SECRET_ID")
export VAULT_TOKEN

# Fetch the database password and export it
export DB_PASSWORD=$(vault kv get -field=password secret/myapp/db)

exec "$@"
EOF
chmod +x entrypoint.sh

The VAULT_ROLE_ID and VAULT_SECRET_ID are non-sensitive identifiers that can be safely passed via environment variables, while the actual secrets are fetched dynamically and never persist in the image.

Step 7: General Best Practices Checklist

  • Never use ENV for secrets in Dockerfiles; use runtime injection instead.
  • Add .env, *.pem, *.key, and credential files to .dockerignore.
  • Use Docker Swarm secrets or Kubernetes Secrets for orchestrated workloads.
  • Rotate secrets regularly; Swarm secrets can be updated without redeploying images.
  • Restrict access to the Docker socket (/var/run/docker.sock) — it is equivalent to root access on the host.
  • Use --read-only and --tmpfs /run/secrets when running standalone containers that need secrets.
  • Audit your images periodically with docker history --no-trunc and tools like trivy for accidental secret exposure.
# Install trivy on RHEL 7 for secret scanning
sudo yum install -y https://github.com/aquasecurity/trivy/releases/download/v0.50.0/trivy_0.50.0_Linux-64bit.rpm
trivy image --scanners secret myapp:latest

Conclusion

Proper secret management is non-negotiable for any container deployment on RHEL 7. The key principle is separation: secrets must never be baked into image layers, stored in environment variables in Dockerfiles, or committed to version control. At runtime, prefer Docker Swarm secrets or file-based injection via /run/secrets. At build time, use BuildKit’s --mount=type=secret to inject sensitive values without trace. Combined with least-privilege container execution and periodic secret rotation, these practices ensure that your containerised applications on Red Hat Enterprise Linux remain secure throughout their lifecycle.