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.
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.
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.
"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