← DevOps← DevOps
DevOpsDevOps18 Th7, 2026Jul 18, 202619 phút đọc16 min read

ContainersContainers

Thuộc bộ tài liệu kiến thức theo DevOps Roadmap.

Tổng quan

Container là kỹ thuật ảo hóa nhẹ, đóng gói ứng dụng cùng toàn bộ dependencies (thư viện, runtime, cấu hình) thành một đơn vị di động duy nhất, chạy nhất quán trên bất kỳ máy chủ nào có kernel tương thích. Khác với máy ảo (VM), container không mang theo hệ điều hành khách — chúng dùng chung kernel của host và cô lập tiến trình bằng các cơ chế có sẵn của Linux. Kết quả: khởi động trong mili giây thay vì vài phút, image chỉ vài chục MB thay vì vài GB, và mật độ triển khai trên mỗi host cao hơn nhiều.

Với DevOps, container giải quyết triệt để vấn đề kinh điển “trên máy tôi chạy được mà”. Image mà developer build ở máy local cũng chính là image được CI kiểm thử và production chạy, loại bỏ hoàn toàn sự lệch môi trường. Container cũng là nền tảng cho mọi thứ phía sau trong roadmap: container orchestration (Kubernetes), immutable infrastructure, GitOps và các pipeline CI/CD hiện đại đều mặc định workload đã được container hóa.

Docker là công cụ phổ biến hóa container, nhưng hệ sinh thái hiện đã chuẩn hóa quanh các đặc tả OCI (Open Container Initiative) cho image và runtime, nên image build bằng Docker chạy được với Podman, containerd, CRI-O và mọi runtime tuân thủ OCI khác.

Một mô hình tư duy

Hãy hình dung image như một class và container như một instance của class đó. Image là một template chỉ-đọc, có phiên bản; còn container là một tiến trình đang chạy (hoặc đã dừng) được tạo ra từ image, có writable layer riêng, PID riêng và góc nhìn cô lập riêng về hệ thống. Bạn build image một lần và chạy nhiều container từ nó. Đây là lý do “rebuild image, deploy lại” mới là cách sửa đúng cho một container hỏng, chứ không phải “SSH vào rồi vá tay” — làm vậy là bạn đang biến đổi một instance mà class của nó không còn mô tả nữa.

Ba ý tưởng chống đỡ mọi thứ còn lại trong trang này:

Kiến thức nền tảng

Container vs máy ảo (VM)

Khía cạnhContainerMáy ảo
Ranh giới cô lậpCơ chế kernel (namespaces, cgroups)Hypervisor + phần cứng ảo
Hệ điều hành kháchKhông có (dùng chung kernel host)Mỗi VM một OS đầy đủ
Thời gian khởi độngMili giây – vài giâyVài giây – vài phút
Kích thước điển hìnhVài MB (image layers)Vài GB (disk image)
Mật độ trên mỗi hostHàng trămHàng chục
Overhead bộ nhớGần như không (chung kernel)Hàng trăm MB cho mỗi guest kernel
Mức độ cô lậpYếu hơn (bề mặt tấn công là kernel chung)Mạnh hơn (kernel riêng)
Lựa chọn kernelPhải khớp kernel hostMỗi VM một OS/kernel tùy ý
Live migrationCòn nonTrưởng thành (vMotion…)
Trường hợp dùngMicroservices, CI, đóng gói ứng dụngMulti-tenancy chặt, cần kernel/OS khác nhau

Hai công nghệ bổ trợ cho nhau: hầu hết cluster production chạy container bên trong VM (ví dụ node EC2 trong cluster EKS). VM cho bạn ranh giới bảo mật cứng giữa các tenant và tự do chọn kernel; container cho bạn khả năng đóng gói workload nhanh và dày đặc bên trên.

Khi nào container là công cụ sai: bạn cần một kernel khác host (container Windows không chạy được trên kernel Linux và ngược lại), bạn cần cô lập multi-tenant cứng cho code không tin cậy (dùng VM, hoặc runtime sandbox như gVisor / Kata Containers / Firecracker microVM), hoặc workload là một monolith gắn chặt với host và không được lợi gì từ việc đóng gói.

Nền tảng Linux bên dưới

Container thực chất là tiến trình Linux bình thường được bọc trong ba cơ chế của kernel:

Hai primitive nữa hoàn thiện bức tranh bảo mật container:

# Tự kiểm chứng: container chỉ là một tiến trình trên host
docker run -d --name web nginx
docker inspect web --format '{{.State.Pid}}'   # PID trên host
sudo ls /proc/<pid>/ns/                          # các namespace của nó (symlink kèm inode id)
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.max   # giới hạn memory cgroup v2
docker top web                                   # các tiến trình nhìn từ host

Kiến trúc Docker

docker CLI ──REST──> dockerd (daemon) ──gRPC──> containerd ──> containerd-shim ──> runc ──> tiến trình container

Mô hình daemon có nhược điểm: dockerd chạy dưới quyền root và là một điểm lỗi đơn (single point of failure). Đó chính là khoảng trống mà các công cụ không cần daemon như Podman lấp vào.

Image, layer và registry

Một image là một manifest trỏ tới danh sách có thứ tự các layer tarball chỉ-đọc, kèm một đối tượng config (entrypoint, env, working dir, user, port). Mỗi chỉ thị Dockerfile làm thay đổi filesystem sẽ tạo một layer. Layer được cache và tái sử dụng: nếu đầu vào của layer không đổi, lần build sau sẽ bỏ qua và dùng lại digest đã cache.

Khái niệm chính

Cấu tạo một Dockerfile

Các chỉ thị phổ biến nhất và tác dụng của chúng:

Chỉ thịTác dụng
FROMBase image (hoặc một stage trước trong multi-stage)
RUNThực thi lệnh lúc build; tạo một layer
COPY / ADDCopy file vào image (ADD còn giải nén/tải URL — nên ưu tiên COPY)
WORKDIRĐặt working directory cho các chỉ thị sau
ENV / ARGBiến env lúc runtime / biến chỉ có lúc build
EXPOSEGhi chú port (chỉ là metadata; không publish)
USERĐặt UID/GID mà RUN, CMD, ENTRYPOINT dùng
ENTRYPOINTFile thực thi mà container chạy
CMDTham số mặc định (hoặc lệnh nếu không có entrypoint)
HEALTHCHECKLệnh runtime dùng để kiểm tra sức khỏe container

ENTRYPOINT vs CMD: dùng ENTRYPOINT ["/app"] cho binary và CMD ["--serve"] cho tham số mặc định có thể override. Luôn ưu tiên exec form (mảng JSON ["/bin/sh", "-c", ...]) hơn shell form — shell form bọc tiến trình của bạn trong /bin/sh -c, khiến sh thành PID 1 và không chuyển tiếp tín hiệu, nên docker stop sẽ treo tới hết timeout rồi mới hard-kill app.

Dockerfile best practices qua ví dụ

# ---- giai đoạn build ----
FROM golang:1.22-alpine AS build
WORKDIR /src
# Copy file khai báo dependency trước: layer này chỉ invalidate khi deps thay đổi
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/app ./cmd/app

# ---- giai đoạn runtime ----
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD ["/app", "healthcheck"]
ENTRYPOINT ["/app"]

Vì sao cách này hiệu quả:

Một cạm bẫy cache qua ví dụ: đặt COPY . . trước RUN npm install nghĩa là mọi thay đổi source đều phá layer cài dependency, buộc cài lại toàn bộ ở mỗi lần build. Hãy copy package.json/package-lock.json trước, cài, rồi mới copy phần còn lại. Nguyên tắc này áp dụng cho cả go.mod, requirements.txt, pom.xml, Cargo.toml.

BuildKit là builder hiện đại (mặc định trong Docker hiện nay). Nó build layer song song, hỗ trợ mount secret lúc build (RUN --mount=type=secret), cache mount (RUN --mount=type=cache,target=/root/.cache) và build đa nền tảng qua buildx.

docker-compose

Compose mô tả ứng dụng nhiều container theo kiểu khai báo — công cụ chuẩn cho môi trường phát triển local và triển khai đơn giản trên một host:

services:
  api:
    build: .
    ports: ["8080:8080"]
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 5
volumes:
  pgdata:

docker compose up -d khởi động tất cả trên một user-defined bridge network chung, nơi các service phân giải nhau bằng tên (db). depends_on với condition: service_healthy chờ health check của DB xanh rồi mới khởi động API — depends_on thường chỉ chờ container khởi động, không chờ app sẵn sàng. Compose tuyệt vời cho dev và CI; ở quy mô production hãy chuyển lên Kubernetes.

Networking và volumes

Network driverHành viKhi nào dùng
bridge (mặc định)Mạng riêng NAT trên host; mở port bằng -p host:containerApp một host, dev local
hostContainer dùng chung network stack với host (không cô lập, không map port)Cần hiệu năng mạng, chấp nhận xung đột port
noneKhông có networkBatch job không cần mạng
overlayMạng đa host, mã hóaService Swarm qua nhiều node
macvlanContainer có MAC/IP riêng trên LAN vật lýApp cũ phải xuất hiện như host thật

bridge mặc địnhuser-defined bridge khác nhau ở một điểm quan trọng: chỉ user-defined bridge mới có service discovery qua DNS theo tên container tự động. Compose và docker network create cho bạn user-defined bridge; bridge mặc định cũ cần --link (đã deprecated).

Lưu trữ:

Filesystem của container vốn dĩ là tạm thời (ephemeral) — mọi thứ không nằm trong volume sẽ mất khi container bị xóa. Writable layer cũng chậm (overhead copy-on-write) và không dành cho I/O nặng; hãy đặt database và các đường nóng lên volume.

Các lựa chọn thay thế Docker

Công cụGhi chú
PodmanKhông cần daemon, mặc định rootless, CLI tương thích Docker (alias docker=podman); chạy mỗi container như con của user gọi lệnh, không có root daemon để bị chiếm. Khái niệm pod gom các container giống Kubernetes Pod, và podman generate kube xuất ra YAML Kubernetes.
BuildahBạn đồng hành build của Podman; build image bằng script, không cần daemon hay thậm chí Dockerfile.
kanikoBuild image từ Dockerfile bên trong một container/K8s pod, không cần daemon đặc quyền — lý tưởng cho CI trên Kubernetes.
BuildKit / buildxBuilder hiệu năng cao hiện đại bên dưới Docker; song song, hiểu cache, đa kiến trúc, hỗ trợ secret.
LXC/LXDSystem container — mang đầy đủ userland của OS (init, syslog, ssh), gần với VM nhẹ hơn là container ứng dụng một-tiến-trình.
containerd + nerdctlnerdctl là CLI tương thích Docker chạy trên chính runtime Kubernetes dùng; tiện để debug trực tiếp trên node.
gVisor / KataCô lập mạnh hơn: gVisor chạy một kernel userspace chặn syscall; Kata chạy mỗi container trong một microVM. Dùng cho workload không tin cậy hoặc multi-tenant.

Các cạm bẫy thường gặp

Best Practices

  1. Dùng multi-stage build — giữ trình biên dịch, dev dependencies và source code ngoài image runtime; image nhỏ hơn thì pull nhanh hơn, khởi động nhanh hơn và ít bề mặt tấn công hơn.
  2. Ghim base image theo tag, tốt nhất là theo digestpython:3.12-slim@sha256:... thay vì python:latest; tag không ghim khiến build không tái lập được và rollback vô nghĩa.
  3. Chạy với user không phải root — thêm USER trong Dockerfile; kết hợp root filesystem chỉ-đọc (--read-only) và --cap-drop=ALL để một lần bị chiếm chỉ có quyền tối thiểu.
  4. Sắp xếp chỉ thị Dockerfile để tận dụng cache — copy file khai báo dependency và cài deps trước khi copy toàn bộ source; đặt nội dung hay thay đổi nhất ở cuối.
  5. Mỗi container một tiến trình/một trách nhiệm — cho phép scale, restart và luồng log độc lập; dùng Compose hoặc Kubernetes Pod cho các tiến trình phụ trợ đi kèm thay vì nhồi nhiều daemon vào một image.
  6. Thêm .dockerignore — loại .git, node_modules, secrets, artifacts khỏi build context; build nhanh hơn và tránh vô tình rò rỉ vào layer.
  7. Không bao giờ nhúng secret vào image — layer là vĩnh viễn và khôi phục được; dùng env var lúc runtime, secret mount, hoặc BuildKit --mount=type=secret cho credential lúc build mà không để lại dấu vết.
  8. Quét lỗ hổng image trong CI — chạy Trivy/Grype ở mọi lần build, fail khi có CVE high/critical, và rebuild định kỳ để bản vá base layer thực sự tới được production.
  9. Sinh SBOM và ký image — tạo software bill of materials (docker sbom, Syft) và ký bằng cosign để bên tiêu thụ xác minh nguồn gốc (bảo mật chuỗi cung ứng).
  10. Khai báo HEALTHCHECK và resource limits — health check giúp orchestrator restart container hỏng; --memory/--cpus (hoặc limits của K8s) tránh một workload “bóp nghẹt” hàng xóm.
  11. Xử lý tín hiệu và dùng init đúng cách — bắt SIGTERM để shutdown mượt và dùng --init/tini để PID 1 reap zombie; nếu không deploy sẽ treo và tiến trình rò rỉ.
  12. Dùng exec form cho ENTRYPOINT/CMD — dạng mảng JSON chạy binary trực tiếp làm PID 1 nên tín hiệu tới được nó; shell form chôn nó dưới /bin/sh -c.
  13. Log ra stdout/stderr — không ghi ra file trong container; để platform (Docker logging driver, Fluent Bit) thu thập và chuyển log.
  14. Coi container là bất biến (immutable) — không docker exec để vá production; rebuild image và deploy lại để state đang chạy luôn khớp với một artifact đã commit.
  15. Giữ state trong volume, không trong container — database và file upload thuộc về named volume hoặc dịch vụ bên ngoài; writable layer là tạm thời và chậm.
  16. Ưu tiên base image nhỏ, đúng mục đíchdistroless/alpine/scratch giảm CVE và thời gian pull; lưu ý Alpine dùng musl libc, có thể gây trục trặc với binary phụ thuộc glibc.
  17. Build image multi-arch khi chạy trên fleet hỗn hợpdocker buildx tạo manifest amd64+arm64 để cùng một tag chạy được trên cả node Graviton lẫn Intel.

Tài liệu tham khảo

Part of the DevOps Roadmap knowledge base.

Overview

Containers are a lightweight virtualization technique that packages an application together with its dependencies (libraries, runtime, configuration) into a single, portable unit that runs consistently on any host with a compatible kernel. Unlike virtual machines, containers do not ship a guest operating system — they share the host’s kernel and isolate processes using native Linux features. The result: startup in milliseconds instead of minutes, images measured in megabytes instead of gigabytes, and far higher density per host.

For DevOps, containers solve the classic “works on my machine” problem. The same image that a developer builds locally is what CI tests and what production runs, eliminating environment drift. Containers are also the foundational building block for everything downstream in the roadmap: container orchestration (Kubernetes), immutable infrastructure, GitOps, and modern CI/CD pipelines all assume containerized workloads.

Docker popularized the format, but the ecosystem is now standardized around the OCI (Open Container Initiative) specifications for images and runtimes, so images built with Docker run under Podman, containerd, CRI-O, and any other OCI-compliant runtime.

A mental model

Think of an image as a class and a container as an instance of that class. The image is a read-only, versioned template; a container is a running (or stopped) process created from it, with its own writable layer, its own PID, and its own isolated view of the system. You build images once and run many containers from them. This is why “rebuild the image, redeploy” is the correct fix for a bad container, never “SSH in and patch it” — you would be mutating an instance whose class no longer describes it.

Three ideas underpin everything else on this page:

Fundamentals

Containers vs Virtual Machines

AspectContainerVirtual Machine
Isolation boundaryKernel features (namespaces, cgroups)Hypervisor + virtual hardware
Guest OSNone (shares host kernel)Full OS per VM
Startup timeMilliseconds–secondsSeconds–minutes
Typical sizeMBs (image layers)GBs (disk image)
Density per hostHundredsTens
Memory overheadAlmost none (shared kernel)Hundreds of MB per guest kernel
Isolation strengthWeaker (shared kernel attack surface)Stronger (separate kernels)
Kernel choiceMust match host kernelAny OS/kernel per VM
Live migrationImmatureMature (vMotion et al.)
Use caseMicroservices, CI, packaging appsStrong multi-tenancy, different OS kernels

They are complementary: most production clusters run containers inside VMs (e.g., EC2 nodes in an EKS cluster). The VM gives you a hard security boundary between tenants and the freedom to pick a kernel; the container gives you fast, dense packing of workloads on top.

When containers are the wrong tool: you need a different kernel than the host (a Windows container cannot run on a Linux kernel and vice versa), you require hard multi-tenant isolation for untrusted code (use a VM, or a sandboxed runtime like gVisor / Kata Containers / Firecracker microVMs), or the workload is a monolith with deep host coupling that gains nothing from packaging.

Linux underpinnings

Containers are ordinary Linux processes wrapped in three kernel mechanisms:

Two more primitives round out container security:

# See it yourself: a container is just a process on the host
docker run -d --name web nginx
docker inspect web --format '{{.State.Pid}}'   # host PID
sudo ls /proc/<pid>/ns/                          # its namespaces (symlinks with inode ids)
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.max   # cgroup v2 memory limit
docker top web                                   # processes as seen from the host

Docker architecture

docker CLI ──REST──> dockerd (daemon) ──gRPC──> containerd ──> containerd-shim ──> runc ──> container process

The daemon model has a downside: dockerd runs as root and is a single point of failure. That is precisely the gap daemonless tools like Podman fill.

Images, layers, and registries

An image is a manifest pointing to an ordered list of read-only tarball layers plus a config object (entrypoint, env, working dir, user, exposed ports). Each Dockerfile instruction that changes the filesystem creates a layer. Layers are cached and reused: if a layer’s inputs haven’t changed, rebuilds skip it and reuse the cached digest.

Key Concepts

Anatomy of a Dockerfile

The most common instructions, and what they do:

InstructionEffect
FROMBase image (or a previous stage in multi-stage)
RUNExecute a command at build time; creates a layer
COPY / ADDCopy files into the image (ADD also untars/fetches URLs — prefer COPY)
WORKDIRSet the working directory for subsequent instructions
ENV / ARGRuntime env var / build-time-only variable
EXPOSEDocument the port (metadata only; does not publish)
USERSet the UID/GID that RUN, CMD, ENTRYPOINT use
ENTRYPOINTThe executable the container runs
CMDDefault arguments (or command if no entrypoint)
HEALTHCHECKCommand the runtime uses to test container health

ENTRYPOINT vs CMD: use ENTRYPOINT ["/app"] for the binary and CMD ["--serve"] for default, overridable arguments. Always prefer the exec form (["/bin/sh", "-c", ...] JSON array) over the shell form — the shell form wraps your process in /bin/sh -c, which becomes PID 1 and does not forward signals, so docker stop will hang until the timeout and hard-kill your app.

Dockerfile best practices in action

# ---- build stage ----
FROM golang:1.22-alpine AS build
WORKDIR /src
# Copy dependency manifests first: this layer only invalidates when deps change
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/app ./cmd/app

# ---- runtime stage ----
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD ["/app", "healthcheck"]
ENTRYPOINT ["/app"]

Why this works:

A worked cache pitfall: putting COPY . . before RUN npm install means every source change busts the dependency-install layer, forcing a full reinstall on every build. Copy package.json/package-lock.json first, install, then copy the rest. The same principle applies to go.mod, requirements.txt, pom.xml, and Cargo.toml.

BuildKit is the modern builder (default in current Docker). It builds layers in parallel, supports build-time secret mounts (RUN --mount=type=secret), cache mounts (RUN --mount=type=cache,target=/root/.cache), and cross-platform builds via buildx.

docker-compose

Compose describes multi-container applications declaratively — the standard tool for local development environments and simple single-host deployments:

services:
  api:
    build: .
    ports: ["8080:8080"]
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 5
volumes:
  pgdata:

docker compose up -d starts everything on a shared user-defined bridge network where services resolve each other by name (db). depends_on with condition: service_healthy waits for the DB’s health check before starting the API — plain depends_on only waits for the container to start, not for the app to be ready. Compose is excellent for dev and CI; for production at scale, graduate to Kubernetes.

Networking and volumes

Network driverBehaviorWhen to use
bridge (default)Private NAT’d network on the host; publish ports with -p host:containerSingle-host apps, local dev
hostContainer shares the host network stack (no isolation, no port mapping)Performance-sensitive networking, when port conflicts are acceptable
noneNo networking at allBatch jobs that need no network
overlayMulti-host encrypted networkingSwarm services across nodes
macvlanContainer gets its own MAC/IP on the physical LANLegacy apps that must appear as real hosts

The default bridge and a user-defined bridge differ in one important way: only user-defined bridges provide automatic DNS-based service discovery by container name. Compose and docker network create give you user-defined bridges; the legacy default bridge requires --link (deprecated).

Storage:

Container filesystems are ephemeral by design — anything not in a volume is lost when the container is removed. The writable layer is also slow (copy-on-write overhead) and not meant for heavy I/O; put databases and hot paths on volumes.

Alternatives to Docker

ToolNotes
PodmanDaemonless, rootless by default, CLI-compatible with Docker (alias docker=podman); runs each container as a child of the invoking user, no root daemon to compromise. Its pod concept groups containers like a Kubernetes Pod, and podman generate kube emits Kubernetes YAML.
BuildahPodman’s build companion; scriptable image builds without a daemon or even a Dockerfile.
kanikoBuilds images from a Dockerfile inside a container/K8s pod with no privileged daemon — ideal for CI on Kubernetes.
BuildKit / buildxThe modern high-performance builder underneath Docker; parallel, cache-aware, multi-arch, secret-aware.
LXC/LXDSystem containers — a full OS userland (init, syslog, ssh), closer to lightweight VMs than to single-process app containers.
containerd + nerdctlnerdctl is a Docker-compatible CLI over the runtime Kubernetes actually uses; handy for debugging nodes.
gVisor / KataStronger isolation: gVisor runs a userspace kernel that intercepts syscalls; Kata runs each container in a microVM. Use for untrusted or multi-tenant workloads.

Common pitfalls

Best Practices

  1. Use multi-stage builds — keep compilers, dev dependencies, and source code out of runtime images; smaller images pull faster, start faster, and expose less attack surface.
  2. Pin base images by tag and ideally digestpython:3.12-slim@sha256:... instead of python:latest; unpinned tags make builds non-reproducible and rollbacks meaningless.
  3. Run as a non-root user — add USER in the Dockerfile; combine with a read-only root filesystem (--read-only) and --cap-drop=ALL so a compromise has minimal privilege.
  4. Order Dockerfile instructions for cache efficiency — copy dependency manifests and install deps before copying the full source tree; put the most frequently changing content last.
  5. One process/concern per container — enables independent scaling, restarts, and log streams; use Compose or Kubernetes Pods for co-located helpers instead of stuffing multiple daemons into one image.
  6. Add a .dockerignore — exclude .git, node_modules, secrets, and build artifacts from the build context; speeds builds and prevents accidental leaks into layers.
  7. Never bake secrets into images — layers are permanent and recoverable; use runtime env vars, mounted secrets, or BuildKit --mount=type=secret for build-time credentials that leave no trace.
  8. Scan images for vulnerabilities in CI — run Trivy/Grype on every build, fail on high/critical CVEs, and rebuild regularly so patched base layers actually reach production.
  9. Generate an SBOM and sign images — produce a software bill of materials (docker sbom, Syft) and sign with cosign so consumers can verify provenance (supply-chain security).
  10. Define HEALTHCHECK and resource limits — health checks let orchestrators restart broken containers; --memory/--cpus (or K8s limits) prevent one workload from starving its neighbors.
  11. Handle signals and use a proper init — trap SIGTERM for graceful shutdown and use --init/tini so PID 1 reaps zombies; otherwise deploys hang and processes leak.
  12. Use the exec form of ENTRYPOINT/CMD — JSON-array form runs your binary directly as PID 1, so signals reach it; the shell form buries it under /bin/sh -c.
  13. Log to stdout/stderr — never to files inside the container; let the platform (Docker logging driver, Fluent Bit) collect and ship logs.
  14. Treat containers as immutable — never docker exec to patch production; rebuild the image and redeploy so the running state always matches a committed artifact.
  15. Keep state in volumes, not the container — databases and uploads belong in named volumes or external services; the writable layer is ephemeral and slow.
  16. Prefer small, purpose-built base imagesdistroless/alpine/scratch reduce CVEs and pull time; be mindful that Alpine uses musl libc, which can trip up glibc-dependent binaries.
  17. Build multi-arch images when targeting mixed fleetsdocker buildx produces amd64+arm64 manifests so the same tag runs on Graviton and Intel nodes alike.

References