DH
4 min read

Secrets Management for a Dockerised Stack: AWS Secrets Manager, SSM, and Local .env Parity

Production-ready pattern: AWS Secrets Manager and SSM Parameter Store for deployed environments, with genuine local .env parity—not aspirational.

dockernodejsdevopsawssecrets

Secrets Management for a Dockerised Stack: AWS Secrets Manager, SSM, and Local .env Parity

Managing secrets across local development, CI, and production is one of those problems that feels solved until you're debugging a 3am incident caused by a stale environment variable. Getting secrets management right in a Dockerised stack is not glamorous work, but it is load-bearing work.

This tutorial walks through a production-ready pattern: AWS Secrets Manager and SSM Parameter Store for deployed environments, with a local .env workflow that stays genuinely in parity — not aspirationally in parity.


Why "Just Use .env" Breaks at Scale

A .env file works on your laptop. Problems emerge with multiple services, multiple developers, a CI pipeline, and multiple deployment environments. You end up with .env.staging, .env.production, secrets scattered across machines, and rotation procedures nobody follows because they're too manual.

Two AWS services address this:

  • AWS Secrets Manager — for secrets requiring rotation (database passwords, API keys, OAuth credentials). The per-secret monthly cost justifies itself through automation.
  • SSM Parameter Store — for configuration values and non-sensitive parameters. The SecureString type encrypts values with KMS, making it viable for secrets, and the standard tier is free.

Use Secrets Manager for anything that rotates or is genuinely sensitive at rest. Use SSM for configuration that varies by environment but isn't catastrophic if exposed.


The Core Pattern: Fetch at Container Startup

The most common mistake: baking secrets into Docker images as build-time arguments. Don't. Instead, fetch secrets at container startup via an entrypoint script.

Here's a minimal entrypoint for a Python service (Django or FastAPI):

#!/bin/sh
set -e

# Fetch secrets from AWS Secrets Manager and export as env vars
SECRET_JSON=$(aws secretsmanager get-secret-value \
--secret-id "$SECRET_NAME" \
--query SecretString \
--output text)

export DB_PASSWORD=$(echo "$SECRET_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['db_password'])")
export API_KEY=$(echo "$SECRET_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['api_key'])")

exec "$@"

Your Dockerfile calls this before the main process:

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "myapp.wsgi"]

The container IAM role grants secretsmanager:GetSecretValue on the specific secret ARN only.


SSM for Per-Environment Configuration

For non-secret, environment-specific values (feature flags, service URLs, log levels), SSM Parameter Store with a naming convention is cleaner:

/myapp/production/LOG_LEVEL
/myapp/staging/LOG_LEVEL
/myapp/production/FEATURE_FLAG_NEW_CHECKOUT

Fetch a whole prefix at once:

aws ssm get-parameters-by-path \
--path "/myapp/$APP_ENV/" \
--with-decryption \
--query "Parameters[*].[Name,Value]" \
--output text | while read name value; do
key=$(basename "$name")
export "$key=$value"
done

Add this to the entrypoint script before your exec call. Configuration and secrets are both resolved at runtime, never baked in.


Achieving Local .env Parity

Most tutorials stop here. They explain AWS and leave your .env drifting from production within a week.

The fix: a script that generates your local .env by pulling from the same SSM path and Secrets Manager secret your containers use:

#!/usr/bin/env bash
# scripts/generate-local-env.sh
set -e

APP_ENV=${1:-development}

echo "# Auto-generated — do not commit" > .env

# Pull SSM config
aws ssm get-parameters-by-path \
--path "/myapp/$APP_ENV/" \
--with-decryption \
--query "Parameters[*].[Name,Value]" \
--output text | while read name value; do
key=$(basename "$name")
echo "$key=$value" >> .env
done

# Pull Secrets Manager values
SECRET_JSON=$(aws secretsmanager get-secret-value \
--secret-id "myapp/$APP_ENV/app-secrets" \
--query SecretString \
--output text)

echo "$SECRET_JSON" | python3 -c "
import sys, json
for k, v in json.load(sys.stdin).items():
print(f'{k.upper()}={v}')
" >> .env

echo ".env generated for $APP_ENV"

Add .env to .gitignore. Commit scripts/generate-local-env.sh to the repo. Every developer pulls from the same source of truth as production. Rotating a secret requires running one script, not finding four .env files.


Docker Compose Wiring

For local development, load the generated .env:

services:
api:
build: .
env_file:
- .env

For deployed environments, the entrypoint script handles everything. Never use env_file in production compose or ECS task definitions.


What This Solves

This pattern gives you a single source of truth (AWS), runtime injection identical across ECS, Kubernetes, or bare Docker, and a local workflow that stays in sync. Secret rotation becomes an AWS operation; containers pick up new values on next restart without image rebuilds.

It requires upfront effort, but it pays back the first time you rotate a compromised key at speed.

Damian Hodgkiss

Damian Hodgkiss

Senior Staff Engineer at Sumo Group, leading development of AppSumo marketplace. Technical solopreneur with 25+ years of experience building SaaS products.

Creating Freedom

Join me on the journey from engineer to solopreneur. Learn how to build profitable SaaS products while keeping your technical edge.

    Proven strategies

    Learn the counterintuitive ways to find and validate SaaS ideas

    Technical insights

    From choosing tech stacks to building your MVP efficiently

    Founder mindset

    Transform from engineer to entrepreneur with practical steps