Implementing Docker Secrets for Secure Environment Variables Without Swarm: A Practical Guide


2 views

When working with sensitive data in Docker containers, many developers face the challenge of securely passing credentials without:

  • Hardcoding in Dockerfiles
  • Exposing in run commands
  • Committing to version control

While Docker Secrets provide an elegant solution, they're traditionally associated with Swarm clusters. Here's how we can achieve similar security in standalone containers.

1. Using Docker Compose with Secrets

Even without Swarm, Docker Compose (version 3.1+) supports secrets:

version: '3.1'
services:
  myapp:
    image: myapp:latest
    secrets:
      - db_password
    environment:
      DB_PASS_FILE: /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Key points:

  • Secrets are mounted at /run/secrets/
  • Use _FILE suffix for environment variables
  • Secret files should have strict permissions (chmod 600)

2. Volume Mounting Secrets

A simple approach using bind mounts:

docker run -v /path/to/secrets:/secrets:ro myapp

Inside your application:

# Python example
with open('/secrets/db_password') as f:
    db_password = f.read().strip()

3. Using .env Files with Caution

While not ideal, you can use .env files with proper precautions:

# .env.production
DB_PASSWORD=supersecret

# docker-compose.yml
env_file:
  - .env.production

Critical security measures:

  • Add .env* to .gitignore
  • Set strict file permissions (chmod 600)
  • Never commit to repositories

Secret Rotation Strategy

Implement a process for regular secret rotation:

#!/bin/bash
# Example rotation script
new_password=$(openssl rand -hex 32)
echo $new_password > /secrets/db_password
docker service update --secret-rm old_password --secret-add source=new_password,target=db_password myapp

Audit Logging

Monitor secret access attempts:

# Sample audit rule for Linux
echo '-w /run/secrets/ -p wa -k docker_secrets' >> /etc/audit/rules.d/docker-secrets.rules

Integration with Vault Solutions

For enterprise-grade security, consider HashiCorp Vault integration:

# Python example using hvac
import hvac
client = hvac.Client(url='https://vault.example.com')
secret = client.read('secret/data/myapp')
db_password = secret['data']['data']['db_password']

When running a single-container Docker application, passing sensitive data via environment variables in the docker run command is common but problematic:

docker run -e DB_PASSWORD=supersecret -e API_KEY=123abc myapp

This approach exposes credentials in:

  • Shell history
  • Process listings (ps aux)
  • Docker inspect output
  • CI/CD logs

1. Using Docker Compose with External Files

Create a secrets directory outside your project:

mkdir -p ~/docker-secrets
echo "supersecret" > ~/docker-secrets/db_password
chmod 600 ~/docker-secrets/*

Then in your docker-compose.yml:

version: '3.8'
services:
  app:
    image: myapp
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ~/docker-secrets/db_password

2. Bind Mounting Secret Files

For pure docker run scenarios:

docker run \
  -v ~/docker-secrets:/run/secrets \
  -e DB_PASSWORD_FILE=/run/secrets/db_password \
  myapp

Your application should then read from the file path specified in the *_FILE environment variable.

3. Using Docker Configs (Even Without Swarm)

While designed for Swarm, configs can work in single-container scenarios:

echo "supersecret" | docker config create db_password -
docker run \
  --config source=db_password,target=/run/secrets/db_password \
  -e DB_PASSWORD_FILE=/run/secrets/db_password \
  myapp

For a Python application, here's how to handle *_FILE pattern:

import os

def get_secret(env_var):
    file_var = f"{env_var}_FILE"
    if file_var in os.environ:
        with open(os.environ[file_var], 'r') as f:
            return f.read().strip()
    return os.getenv(env_var)

db_password = get_secret('DB_PASSWORD')

For Node.js applications:

const fs = require('fs');

function getSecret(envVar) {
    const fileVar = ${envVar}_FILE;
    if (process.env[fileVar]) {
        return fs.readFileSync(process.env[fileVar], 'utf8').trim();
    }
    return process.env[envVar];
}

const apiKey = getSecret('API_KEY');
  • Set strict permissions (600) on secret files
  • Never commit secret files to version control
  • Rotate secrets regularly
  • Use .dockerignore to prevent accidental inclusion
  • Consider using a secrets manager (Hashicorp Vault, AWS Secrets Manager) for production