TheThunderclap
DevOps Docker Security

Docker Security Best Practices

Harden your containers in production: non-root users, read-only filesystems, secrets management, and image scanning.

A

Arjun Bose

Security Engineer

📅 18 January 2025
⏱ 9 min read

Introduction

Containers are not inherently secure. A misconfigured container running as root can give attackers full access to the host. This guide walks through the practical measures that reduce your attack surface significantly.

Run as Non-Root

By default, Docker containers run as root. If an attacker escapes the container, they get root on the host. Always create and use a dedicated non-root user.

Dockerfile.secure
dockerfile
                                            FROM node:20-alpine

"hl-keyword">class="hl-comment"># Create a group and user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app
COPY --chown=appuser:appgroup . .
RUN npm ci --only=production

"hl-keyword">class="hl-comment"># Switch to non-root
USER appuser

EXPOSE 3000
CMD ["node", "server.js"]
                                        

Read-Only Filesystem

Mount the root filesystem as read-only and explicitly allow writes only where needed (e.g., /tmp). This prevents attackers from modifying the application at runtime.

docker-compose.secure.yml
yaml
                                            services:
  app:
    image: myapp:latest
    read_only: true                 "hl-keyword">class="hl-comment"># Root FS is read-only
    tmpfs:
      - /tmp:size=64m,mode=1777    "hl-keyword">class="hl-comment"># Allow writes to /tmp only
    security_opt:
      - no-"hl-keyword">new-privileges:true     "hl-keyword">class="hl-comment"># Prevent privilege escalation
    cap_drop:
      - ALL                        "hl-keyword">class="hl-comment"># Drop ALL Linux capabilities
    cap_add:
      - NET_BIND_SERVICE           "hl-keyword">class="hl-comment"># Re-add only what you need
                                        

Secrets Management

Never bake secrets into images or pass them as environment variables (they appear in docker inspect). Use Docker Secrets (Swarm) or mount them at runtime from a vault.

secrets.ts
typescript
                                            "hl-keyword">import { SecretsManagerClient, GetSecretValueCommand } "hl-keyword">from '@aws-sdk/client-secrets-manager';

"hl-keyword">const client = "hl-keyword">new SecretsManagerClient({ region: 'ap-south-1' });

"hl-keyword">export "hl-keyword">async "hl-keyword">function getSecret(secretName: "hl-type">string): "hl-type">Promise<"hl-type">string> {
  "hl-keyword">const response = "hl-keyword">await client.send(
    "hl-keyword">new GetSecretValueCommand({ SecretId: secretName })
  );
  "hl-keyword">if (!response.SecretString) "hl-keyword">throw "hl-keyword">new Error('Secret not found');
  "hl-keyword">return response.SecretString;
}

"hl-keyword">class="hl-comment">// At startup, fetch secrets once
"hl-keyword">const DB_PASSWORD = "hl-keyword">await getSecret('prod/myapp/db-password');
"hl-keyword">class="hl-comment">// Never log or expose DB_PASSWORD after this point
                                        

💬 Comments

0 comments

Leave a comment

0/1000

Comments are moderated. Be respectful. ✌️

📚 Related Articles