What Docker is and why teams adopt it
Docker packages an application, its libraries and its runtime settings into an image. A container is a running instance of that image, isolated from its neighbours by Linux namespaces and control groups. The appeal is consistency: the artefact you test is the artefact you deploy, and a new engineer can run the whole stack with one command.
That promise holds only if the image is built well. Our Docker containerisation service exists because most teams adopt Docker quickly, then live with the consequences of early Dockerfiles for years. This page covers how we approach Docker consulting for teams that already ship containers or are about to.
How we build images for production
Multi-stage builds and base images
A production image should contain the application and what it needs to run, nothing else. Multi-stage builds do this by compiling in one stage and copying artefacts into a clean final stage. Our worked guidance is in Dockerfile multi-stage builds and Docker image size reduction.
Base images deserve a policy. We pin by version, prefer a small image whose libc matches your dependencies, and decide who rebuilds images when a base is patched. An unpinned latest tag is the most common cause of builds that work on Monday and fail on Tuesday.
# syntax=docker/dockerfile:1
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -o /out/app ./cmd/app
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot
ENTRYPOINT ["/app"]
The cache mounts keep module and build caches between runs, and the final image has no shell or package manager. Distroless images need a debugging plan (ephemeral debug containers in Kubernetes work well), so we agree that up front.
Layer order and build context
Docker caches each instruction. Copy dependency manifests first, install, then copy source, and a code change reuses the expensive layer. A .dockerignore file keeps .git, local dependencies and test data out of the build context, which speeds every build and prevents accidental leaks. See dockerignore and layer caching.
Security and supply chain
Image security is mostly discipline. We run containers as a non-root user, drop Linux capabilities the application does not need, and use a read-only root filesystem where possible. Secrets never go into ARG, ENV or COPY, because every layer is readable by anyone who can pull the image.
In CI we scan with Trivy, fail builds on severities you have agreed, and sign images with Cosign so the cluster can verify what it runs. The detail is in container security hardening and image signing with Cosign. Scanning is a snapshot: a clean image today can have a published CVE tomorrow, so we also rebuild on a schedule.
Cost and capacity
Docker itself is free to use for most teams, but images cost money in three places: registry storage, data transfer and slow pipelines. Lifecycle rules that expire untagged and old images keep storage predictable. Pull-through caches and regional replication cut cross-region transfer. Smaller images also shorten node start times, which matters when Karpenter or another autoscaler adds capacity under load.
Check Docker Desktop licensing for your organisation if developers use it. Some companies move to alternatives such as Podman or Colima for local work, which is a legitimate choice as long as images stay OCI compliant.
Day-2 operations
Operating Docker well means treating images like software with a lifecycle:
- Rebuild base images on a schedule and roll the updates through CI, rather than waiting for a vulnerability to force it.
- Monitor image pull failures and registry rate limits, which are a classic cause of failed deployments.
- Keep Dockerfile linting (hadolint) in the pipeline so standards survive team changes.
- Track image provenance so you can answer which commit built the image running in production.
Our build pipeline integration work wires these into your existing CI.
Build tool, runtime and local development
It helps to separate three roles that the word Docker covers. Docker Engine and BuildKit build images. A container runtime such as containerd runs them on servers and Kubernetes nodes. Docker Desktop or an alternative gives developers a local environment. Teams get into trouble when they assume all three must be the same product.
For local work we usually standardise on Compose files that start the application plus its databases and queues, with health checks so dependencies are ready before the app connects. Keeping those definitions close to the production configuration reduces the classic gap where something works locally and fails in a cluster. See Docker Compose for local development for how we structure it.
Common failure modes
- Giant images from build tools left in the final stage, causing slow deploys and large scan reports.
- Root by default, which turns a small application bug into a larger incident.
- Mutable tags such as
latest, which make rollbacks guesswork. Use immutable tags or digests. - PID 1 problems: shell-form
CMDswallows signals, so containers ignore termination and are killed after the grace period. Use exec form. - Memory limits ignored by the runtime, for example JVM or Node settings that assume the whole host. We set limits and runtime flags together.
- Compose in production without a plan for restarts, rollouts or host failure.
When Docker is not the answer
Not every workload needs a container you maintain. Small event-driven tasks fit serverless functions better. If a team of five runs three services, a managed container service such as ECS on Fargate or Cloud Run may beat Kubernetes on effort. When you do outgrow Compose, we help with moving from Compose to Kubernetes, and package the result with Helm on a cluster such as Amazon EKS. If you want a second opinion on your current images, get in touch.
Official documentation: docs.docker.com