Skip to content
Back to Blog
DevOps

Container Security Best Practices for Production

Secure Docker and Kubernetes deployments: image scanning, non-root containers, seccomp, AppArmor, network policies, and runtime security.

Jan 2026
16 min read

Introduction

Container security is not automatic — a Docker container running as root with no resource limits and a publicly pulled image is a significant security risk. Enterprise container deployments require deliberate security hardening at every layer: image security, runtime security, network isolation, and secret management. This guide teaches practical container security for production environments.

Image Security

The most common container security mistake: using unvetted base images.

DOCKERFILE
# Bad: using latest (unpinned) base image
FROM ubuntu:latest  # Could change any day

# Bad: running as root (default)
COPY app /app
CMD ["/app"]

# Good: pinned version, non-root user
FROM ubuntu:22.04@sha256:2b7412e6465c3c7fc5bb21d3e6f1917c167358449fecac8176c6e496e5c1f05f

# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser

# Set proper ownership
COPY --chown=appuser:appuser . /app
WORKDIR /app

# Drop ALL Linux capabilities, only add what's needed
USER appuser

# Don't run as PID 1 (use tini or dumb-init)
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/app/server"]

Scanning Images for Vulnerabilities

BASH
# Trivy: scan for CVEs, misconfigurations, secrets
trivy image company/myapp:v1.2.3

# Output example:
# Total: 15 (CRITICAL: 2, HIGH: 5, MEDIUM: 8)
# Library         Vulnerability   Severity  Installed   Fixed
# libssl1.1       CVE-2021-3711   CRITICAL  1.1.1k      1.1.1l

# Integrate into CI pipeline
trivy image --exit-code 1 --severity CRITICAL company/myapp:latest
# Fails pipeline if CRITICAL CVEs found

# Scan Dockerfile for misconfigurations
trivy config ./Dockerfile

# Snyk alternative
snyk container test company/myapp:latest

# Docker Scout (built into Docker Desktop)
docker scout cves company/myapp:latest

Runtime Security with Seccomp and AppArmor

BASH
# Seccomp: restrict system calls available to container
# Create seccomp profile
cat > /etc/docker/seccomp/myapp.json << 'EOF'
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    {
      "names": [
        "accept4", "bind", "brk", "clock_gettime",
        "close", "connect", "epoll_create1", "epoll_ctl",
        "epoll_wait", "exit_group", "fcntl", "fstat",
        "futex", "getpid", "listen", "mmap", "mprotect",
        "munmap", "nanosleep", "poll", "read", "recvfrom",
        "sendto", "set_robust_list", "setsockopt", "sigaltstack",
        "socket", "stat", "write"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}
EOF

# Run with seccomp profile
docker run --security-opt seccomp=/etc/docker/seccomp/myapp.json company/myapp:latest

# AppArmor profile (Ubuntu/Debian)
# /etc/apparmor.d/docker-myapp
profile docker-myapp flags=(attach_disconnected, mediate_deleted) {
  network tcp,
  network udp,
  deny network raw,
  file /app/** r,
  file /tmp/** rw,
  deny /etc/shadow r,
  deny @{PROC}/sysrq-trigger rwmlk,
}

apparmor_parser -r -W /etc/apparmor.d/docker-myapp
docker run --security-opt apparmor=docker-myapp company/myapp:latest

Kubernetes Pod Security

YAML
# Pod Security Context: enforce non-root, read-only filesystem
apiVersion: v1
kind: Pod
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault

  containers:
  - name: app
    image: company/myapp:v1.2.3
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true    # Immutable container filesystem
      capabilities:
        drop: ["ALL"]
        add: ["NET_BIND_SERVICE"]     # Only add what's needed

    resources:
      limits:
        cpu: "500m"
        memory: "512Mi"
      requests:
        cpu: "100m"
        memory: "128Mi"

    volumeMounts:
    - name: tmp-dir
      mountPath: /tmp              # Writable tmp

  volumes:
  - name: tmp-dir
    emptyDir: {}

Network Security in Containers

YAML
# NetworkPolicy: deny all, allow only needed
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: app-network-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: myapp
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: nginx-ingress  # Only ingress controller can reach app
    ports:
    - protocol: TCP
      port: 8080
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: postgres      # App can only talk to postgres
    ports:
    - protocol: TCP
      port: 5432
  - to:
    - namespaceSelector: {}  # Allow DNS queries
    ports:
    - protocol: UDP
      port: 53

Secrets Management

BASH
# Never pass secrets as environment variables (visible in ps, docker inspect, logs)
docker run -e DATABASE_PASSWORD=secret123 myapp  # BAD

# Better: Docker secrets (Swarm)
echo "secret123" | docker secret create db_password -
docker service create --secret db_password myapp
# Secret available at: /run/secrets/db_password

# Best: Vault Agent sidecar (see hashicorp-vault-secrets guide)
# Or: Kubernetes Secrets with external-secrets operator
kubectl create secret generic db-creds   --from-literal=password=secret123   --namespace production

Falco: Runtime Threat Detection

YAML
# Falco detects suspicious container behavior
# /etc/falco/falco_rules.yaml
- rule: Shell in Container
  desc: Detect shell execution in a container
  condition: container and proc.name in (bash, sh, ash, zsh) and not proc.pname in (runc)
  output: "Shell spawned in container (user=%user.name container=%container.name cmd=%proc.cmdline)"
  priority: WARNING

- rule: Write below /etc
  desc: Detect file writes to /etc
  condition: container and fd.name startswith /etc and evt.type = write
  output: "Write to /etc in container (container=%container.name file=%fd.name)"
  priority: ERROR

- rule: Outbound Connection to Rare IP
  desc: Detect unexpected outbound connections
  condition: >
    outbound and container and
    not fd.sip in (allowed_outbound_destinations)
  output: "Unexpected outbound connection (container=%container.name ip=%fd.sip)"
  priority: WARNING

Container security is defense-in-depth: start with non-root images and vulnerability scanning in CI, add resource limits and network policies in Kubernetes, and use runtime security tools to detect anomalies. No single control is sufficient — security comes from the combination.