Project Setup Best Practices
This is a working checklist for how a repository should be set up to build, scan, and ship container images.
It assumes one image per deployable, per-service Dockerfiles co-located with code, and BuildKit as the default backend in Docker Engine 29.
How to Use This List
Read the groups top to bottom; they roughly follow the order you would harden a repo.
Each bullet leads with the practice in bold, then the reason.
Adopt them incrementally - a .dockerignore and pinned bases deliver most of the value on day one, and signing plus admission control come as the org matures.
A - Repository Structure
- One
Dockerfileper deployable, co-located with its code. The image boundary is visible in the tree and ownership is unambiguous. - Keep a root
.dockerignoreyou actually maintain. It shrinks the build context and keeps.git,node_modules, and.envfiles out of layers. - Choose the context deliberately. Use the service folder for standalone services and the repo root for monorepo services that
COPYshared packages. - Put shared code in
packages/and services inservices/. Predictable paths make selectiveCOPYand cache ordering easy to reason about.
B - Dockerfile Discipline
- Use multi-stage builds. Keep compilers and dev dependencies in a build stage so the runtime image stays small.
- Copy manifests and install before copying source. This keeps the dependency layer cached across code edits and is the biggest build-time lever.
- Run as a non-root user. Set a numeric
USER(for exampleUSER 10001) so the container cannot run as root by default. - Pin bases by digest.
FROM registry.internal/base/node:22@sha256:...makes builds reproducible and defeats tag drift. - Use
RUN --mount=type=secretfor build credentials. Secrets stay out of layers instead of being baked in byCOPYorARG.
C - Build, Scan, Push Targets
- Wrap builds in a
Makefileorjustso one command is canonical. Developers and CI run the samebuild,scan, andpush, so behavior does not drift. - Tag images with the git SHA.
TAG := $(shell git rev-parse --short HEAD)ties every image back to a commit for traceability. - Gate on a vulnerability scan. Run Trivy or Grype with a non-zero exit on fixable HIGH/CRITICAL CVEs before push.
- Push only from CI, to an internal registry. Central control over what gets published beats ad-hoc laptop pushes.
IMAGE ?= registry.internal/api
TAG ?= $(shell git rev-parse --short HEAD)
build:
docker build -f services/api/Dockerfile -t $(IMAGE):$(TAG) .
scan:
trivy image --exit-code 1 --severity HIGH,CRITICAL $(IMAGE):$(TAG)
push: scan
docker push $(IMAGE):$(TAG)D - Reproducibility & Supply Chain
- Pin dependency versions with lockfiles. A build should resolve the same packages today and in six months.
- Sign images with cosign. Signing the pushed digest lets the cluster verify provenance at admission.
- Generate and attach an SBOM. BuildKit can emit an SBOM attestation for audits and CVE triage.
- Enforce signatures at admission. A policy controller (Kyverno or similar) rejects unsigned images so only your pipeline's output runs.
E - Local Development
- Use Compose v2 for the inner loop, not for production.
docker composewires databases and dependencies locally; it does not define your deployment. - Bind-mount source against a dev stage for fast iteration.
target: buildplus a volume reflects edits without a rebuild. - Never point the cluster at Docker Engine as its runtime. Docker builds images; containerd via CRI runs pods on nodes, since dockershim was removed.
- Keep dev-only tooling out of the production stage. The final image ships the runtime and nothing else.
When You Are Done
You should have per-service Dockerfiles, a maintained root .dockerignore, multi-stage non-root builds, and digest-pinned bases.
Your build/scan/push should live in one Makefile or just file that CI and laptops share, with a CVE gate before push.
Images should be signed and, ideally, verified at admission, so the cluster only runs artifacts your pipeline produced.
FAQs
Makefile or just? Either works; the point is a single canonical entry point for build, scan, and push. just reads more cleanly, make is universal.
Where should the scan run? In CI, before push, with a non-zero exit on fixable HIGH/CRITICAL findings so vulnerable images never reach the registry.
Do I pin by tag or digest? Publish a readable tag but pin the digest in FROM and in manifests, so builds and deployments are reproducible.
Is Compose part of production? No. Compose is a local development tool; production is Kubernetes manifests or Helm running containerd on nodes.
How much of this is day-one work? A .dockerignore, multi-stage non-root builds, pinned bases, and a wrapped build/scan/push are the high-value basics. Signing and admission come later.
Related
- Where Docker Fits in a Repository
- Repo Layout Basics
- Monorepo Build Contexts
- Base Image Internal Registry
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).