Home Services Cloud Security DevSecOps Cloud Deployments Packages Blog About Contact Us
Get a Free Audit
Container Security

Docker Security Best Practices: From Dockerfile to Production

MA Muhammad Amish
· June 1, 2026 · 11 min read

Docker containers have completely revolutionized modern application scaling. Packaging microservices into isolated portable nodes allows developers to test applications locally and deploy them instantly onto distributed production clusters.

However, the isolation provided by standard Docker container models is **not a security boundary by default**. If a containerized application carries runtime security flaws and runs with default configurations, an attacker can execute a container breakout—compromising the host server kernel and breaching the private datacenter network.

To prevent this, security must be built directly into the container lifecycle. In this guide, we provide a complete engineering walkthrough to container hardening, from compiling secure Dockerfiles to configuring safe production parameters.

1. The Danger of Permissive Base Images

Every Docker container compiles from a specified base OS image (e.g. `FROM ubuntu` or `FROM node`). By default, many developers select broad, all-purpose base images to simplify dependency management.

However, permissive base images carry hundreds of standard system binaries, debug utilities (like curl, wget, or netcat), and compiler tools that your production app **never requires**. This increases your **Attack Surface Area** significantly.

If an attacker triggers an injection exploit inside your container, they can leverage the native pre-installed curl or netcat packages to download custom malware or execute reverse shells.

The Solution: Minimalist and Distroless Bases. We replace generic base images with minimal options like **Alpine Linux** (weighing only 5MB) or, ideally, Google's **Distroless** images. Distroless images contain only your compiled application and its specific runtime dependencies—carrying **zero shell systems, package managers, or terminal binaries**. If there is no shell pre-installed, an attacker cannot execute terminal commands, neutralizing standard shell exploits completely.

2. Hardening via Multi-Stage Builds

When building modern applications (like Node.js, Go, or Java), you require heavy SDKs, package managers (like npm or maven), and code compilers during the build phase. However, these tools are completely redundant once the application is compiled.

By using **Docker Multi-Stage Builds**, we separate the build environment from the final execution container:

  • Stage 1 (Build): Leverages a complete SDK container to pull libraries, compile source code, and run tests.
  • Stage 2 (Release): Pulls a minimal, secure runner container (like alpine or distroless) and copies **only the compiled binaries** from Stage 1.

This keeps compiler tools, system access configurations, and development libraries entirely out of the production build, reducing container image sizes from 1 GB down to under 50 MB, while removing major vulnerability pathways.

3. Enforcing Non-Root Execution

By default, unless specified otherwise, Docker containers execute processes using the **root** user account (UID 0).

Because Docker processes share the underlying host server's kernel, the root user inside the container carries equivalent privilege weights. If an attacker triggers a container breakout exploit, they immediately carry root privileges over the physical host machine, gaining full control of the host server.

We prevent this by explicitly declaring a dedicated, non-privileged system user inside the Dockerfile and switching execution domains before running the application:

Dockerfile Non-Root User Declaration

# Create a secure system group and user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Set permissions over application directories
WORKDIR /app
COPY --chown=appuser:appgroup . .

# Switch execution context
USER appuser
        

Enforcing non-privileged user isolation ensures that even if an exploit is triggered, the attacker remains trapped within restricted user boundaries, preventing system takeover.

4. Restricting Runtime Linux Capabilities

Even when running non-root users, Docker containers carry several native Linux kernel capabilities by default, enabling standard network calls, raw sockets, and permission alterations.

However, standard web applications rarely require privileges to modify system clocks, alter routing tables, or bind low-level system ports.

We implement runtime hardening at the orchestrator layer (such as Kubernetes pods or docker run execution commands) by dropping **all** default Linux capabilities and granting back only the exact single capability required (e.g. `NET_BIND_SERVICE` if binding to custom ports):

Hardened Docker Run Runtime Execution

# Drop all capabilities and restrict system resource allocation
docker run --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --security-opt=no-new-privileges:true \
  --memory="512m" --cpus="1.0" \
  -d -p 8080:8080 qloudsec/app:latest
        

5. Read-Only Root Filesystems

When an attacker breaches a container, they usually attempt to download custom scripts, write malicious temporary files, or overwrite system libraries inside `/tmp` or application execution directories.

We neutralize this attack vector by running the production container with a **Read-Only Root Filesystem**.

This locks down the container's disk completely. The OS filesystem is read-only at the system level. If your application specifically requires writing temporary session tokens, we mount separate, highly isolated, in-memory **tmpfs** partitions targeting exact directory paths, keeping the rest of the disk entirely unalterable.

6. Dockerfile: Before and After Hardening

Below, we share a head-to-head comparison detailing a standard, highly vulnerable Node.js Dockerfile alongside its fully hardened, multi-stage, non-root, and minimal equivalent:

Vulnerable Dockerfile (Standard)

# Heavy base with curl, compilers, package tools
FROM node:18

WORKDIR /app

# Copies everything including dev dependencies
COPY . .

RUN npm install

# Runs implicitly as root user
EXPOSE 3000
CMD ["node", "server.js"]
          

Issues: Runs as root, carries all development compiler packages, has complete system shells, easily vulnerable to container breakout exploits.

Hardened Dockerfile (QloudSec Standard)

# Stage 1: Build Environment
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: Minimal Release Environment
FROM node:18-alpine AS runner
WORKDIR /app

# Enforce secure system user isolation
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Copy ONLY compiled production assets
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --chown=appuser:appgroup . .

# Restrict runtime user permissions
USER appuser

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

Highlights: Dual multi-stage architecture, non-privileged execution context, zero development dependencies, and strict file ownership bindings.

7. Execution & Infrastructure Auditing

Establishing container hardening standards represents a vital layer of your comprehensive defense-in-depth framework. Standardizing minimal parent base images, executing non-privileged UID runtimes, and dropping unnecessary Linux system capabilities ensures that even in the face of source code vulnerabilities, the blast radius is minimal.

These configurations satisfy international container orchestrator audits and conform strictly to localized bank transaction audit criteria.

Need help hardening your container fleet, setting up Kubernetes pod security standards, or passing compliance audits? QloudSec provides specialized DevSecOps auditing and microservice security consulting across Pakistan.