Your application needs a database host, a port number, an API key, and a password. How do you pass these to your containers in Kubernetes?

You should not hardcode them in the Docker image. You should not put passwords in your Deployment YAML either.

Kubernetes has two objects for this:

  • ConfigMap — for non-sensitive configuration
  • Secret — for sensitive data like passwords and API keys

Why Not Hardcode Config?

Imagine you hardcode a database hostname in your Docker image:

DB_HOST = "prod-db.internal"

Now you cannot use the same image in staging. You cannot change the hostname without rebuilding the image. And when the hostname changes, you have to redeploy everything.

The correct approach: build once, configure at deploy time. The image stays the same. The configuration is injected when the Pod starts.

ConfigMaps

A ConfigMap stores key-value pairs of non-sensitive configuration data.

Use ConfigMaps for:

  • Database hostnames and ports
  • Feature flags
  • Log levels
  • App configuration files

Creating a ConfigMap

From the command line:

kubectl create configmap myapp-config \
  --from-literal=DB_HOST=postgres \
  --from-literal=DB_PORT=5432 \
  --from-literal=LOG_LEVEL=info

From a YAML file:

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  DB_HOST: "postgres"
  DB_PORT: "5432"
  LOG_LEVEL: "info"
kubectl apply -f configmap.yaml
kubectl get configmaps
kubectl describe configmap myapp-config

From a file (for config files):

# app.conf is a configuration file you want to mount
kubectl create configmap app-config --from-file=app.conf

Using ConfigMaps as Environment Variables

Inject all keys as environment variables using envFrom:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myapp:1.0
          envFrom:
            - configMapRef:
                name: myapp-config    # Inject all keys from the ConfigMap

Or inject specific keys with valueFrom:

containers:
  - name: myapp
    image: myapp:1.0
    env:
      - name: DATABASE_HOST          # Variable name in the container
        valueFrom:
          configMapKeyRef:
            name: myapp-config       # ConfigMap name
            key: DB_HOST             # Key to read from the ConfigMap

Using ConfigMaps as Mounted Files

Some apps read configuration from files, not environment variables. Mount a ConfigMap as a file:

# configmap-nginx.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
data:
  nginx.conf: |
    server {
      listen 80;
      location / {
        return 200 'Hello from nginx!';
      }
    }
# Deployment that mounts the config file
spec:
  containers:
    - name: nginx
      image: nginx:alpine
      volumeMounts:
        - name: nginx-config
          mountPath: /etc/nginx/conf.d/default.conf
          subPath: nginx.conf
  volumes:
    - name: nginx-config
      configMap:
        name: nginx-config

Secrets

A Secret stores sensitive data — passwords, API keys, TLS certificates. Secrets work like ConfigMaps, but Kubernetes handles them differently: they are stored separately in etcd and can be encrypted at rest.

Important: Base64 is Not Encryption

Secret values are stored as base64-encoded strings. This is not encryption — it is just encoding. Anyone with access to the Secret can decode it instantly.

echo -n "mysecretpassword" | base64
# Output: bXlzZWNyZXRwYXNzd29yZA==

echo "bXlzZWNyZXRwYXNzd29yZA==" | base64 -d
# Output: mysecretpassword

For real encryption, use an external secrets manager. We cover that in the production section below.

Creating a Secret

From the command line (Kubernetes base64-encodes automatically):

kubectl create secret generic db-credentials \
  --from-literal=DB_USER=alex \
  --from-literal=DB_PASSWORD=supersecret123

From a YAML file (you must base64-encode manually):

# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  DB_USER: YWxleA==                    # base64 of "alex"
  DB_PASSWORD: c3VwZXJzZWNyZXQxMjM=  # base64 of "supersecret123"

You can also use stringData to skip base64 (Kubernetes encodes it for you):

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
stringData:
  DB_USER: "alex"
  DB_PASSWORD: "supersecret123"   # Kubernetes will encode this
kubectl apply -f secret.yaml
kubectl get secrets
kubectl describe secret db-credentials   # Values are hidden in output

Secret Types

TypeUse case
OpaqueGeneric key-value pairs (most common)
kubernetes.io/tlsTLS certificate and key
kubernetes.io/dockerconfigjsonDocker registry credentials
kubernetes.io/service-account-tokenService account tokens

Using Secrets as Environment Variables

containers:
  - name: myapp
    image: myapp:1.0
    env:
      - name: DB_USER
        valueFrom:
          secretKeyRef:
            name: db-credentials
            key: DB_USER
      - name: DB_PASSWORD
        valueFrom:
          secretKeyRef:
            name: db-credentials
            key: DB_PASSWORD

Or inject all keys at once:

containers:
  - name: myapp
    image: myapp:1.0
    envFrom:
      - secretRef:
          name: db-credentials

Using Secrets as Mounted Files

Mount a Secret as files in a directory. This is useful for TLS certificates or config files that contain credentials.

containers:
  - name: myapp
    image: myapp:1.0
    volumeMounts:
      - name: db-creds
        mountPath: /etc/db-creds
        readOnly: true
volumes:
  - name: db-creds
    secret:
      secretName: db-credentials

This creates files at /etc/db-creds/DB_USER and /etc/db-creds/DB_PASSWORD. The app reads the files to get the values.

Complete Example: Web App + Database

Here is a realistic example combining ConfigMaps and Secrets:

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  DB_HOST: "postgres-service"
  DB_PORT: "5432"
  DB_NAME: "myappdb"
---
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: myapp-secrets
type: Opaque
stringData:
  DB_USER: "alex"
  DB_PASSWORD: "supersecret123"
---
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myapp:1.0
          envFrom:
            - configMapRef:
                name: myapp-config    # Non-sensitive config
            - secretRef:
                name: myapp-secrets   # Sensitive config
          resources:
            requests:
              cpu: "100m"
              memory: "64Mi"
            limits:
              cpu: "250m"
              memory: "128Mi"
kubectl apply -f configmap.yaml -f secret.yaml -f deployment.yaml

Verify the environment variables are set:

# Get a pod name first
kubectl get pods -l app=myapp

# Then exec into the pod
kubectl exec -it <pod-name> -- env | grep DB_

Immutable ConfigMaps and Secrets

For performance, you can make a ConfigMap or Secret immutable. Kubernetes stops watching these objects for changes, which reduces API server load significantly in large clusters.

apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config-v1
immutable: true
data:
  LOG_LEVEL: "info"

Once immutable, you cannot update it. Create a new ConfigMap (e.g., myapp-config-v2) and update your Deployments to reference it.

Production: Use External Secret Managers

For production, do not store real secrets in Kubernetes Secret YAML files. Anyone with git clone access can read them.

The best practice is to use an external secrets manager:

  • External Secrets Operator (ESO) — the most popular solution; syncs secrets from AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, or GCP Secret Manager into Kubernetes Secrets automatically

  • Sealed Secrets — encrypts secrets with a public key so you can safely commit them to git

Example with External Secrets Operator:

# ExternalSecret pulls from AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-store
    kind: SecretStore
  target:
    name: db-credentials         # Creates a Kubernetes Secret with this name
  data:
    - secretKey: DB_PASSWORD
      remoteRef:
        key: myapp/prod/db
        property: password

This way, the actual secret value never appears in git or YAML files.

Common Mistakes

Thinking base64 = encryption

Base64 is encoding, not encryption. A Kubernetes Secret is only as secure as your cluster’s access controls and etcd encryption. Always use external secret managers for production.

Committing Secret YAML files to git

Even with base64 encoding, anyone can decode it. Never commit Secret YAML with real values to git. Use .gitignore to exclude them, or use Sealed Secrets / External Secrets Operator.

Hardcoding secrets as env vars in Deployment YAML

This is worse than using Secrets:

# WRONG — do not do this
env:
  - name: DB_PASSWORD
    value: "supersecret123"   # Visible to anyone who reads the Deployment YAML

Always use secretKeyRef or secretRef to reference a Secret object.

What’s Next?

Your app is running with proper configuration. But what about the database? Databases need persistent storage that survives Pod restarts. Let us cover that next.

Next: Kubernetes Tutorial #5: Persistent Volumes and Storage