Repo Layout Basics
This section shows the concrete file layout for containerizing services: one Dockerfile per deployable, co-located with its code, plus the supporting files that make builds reproducible.
The examples move from a single service up to a small multi-service repo you can build and run.
Prerequisites
- Docker Engine 29.x with BuildKit as the default backend (bundled since Docker Engine 23).
- Compose v2 for local orchestration - invoked as
docker compose, not the legacydocker-compose. - A shell and
git. On Debian/Ubuntu the official packages install both the engine and the Compose plugin.
# Debian/Ubuntu - install Engine + Compose plugin from Docker's repo
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
docker version && docker compose versionBasic Examples
1. One Dockerfile per deployable
Each service that ships as its own image gets its own Dockerfile, next to the code it builds.
services/
api/
Dockerfile
src/
worker/
Dockerfile
src/- The image boundary is visible in the tree: one folder, one
Dockerfile, one image. - Ownership is obvious - the team that owns
api/owns its build. - Two services never share a
Dockerfile; shared behavior belongs in a base image instead. - Standalone services can build with the service folder as context.
2. Co-locate the Dockerfile with service code
Keep the Dockerfile beside src/, not in a top-level docker/ dump.
docker build -t api:dev services/api- The trailing path is the build context; here it is the service folder.
COPY src ./srcresolves relative to that context root.- Reviewers see build and code changes in the same directory diff.
3. Add a .dockerignore
A .dockerignore at the context root prunes files before they are sent to the build.
.git
node_modules
**/*.env
dist
coverage- Shrinks the context, so builds transfer faster.
- Keeps secrets like
.envand history like.gitout of image layers. - Lives at the context root, not next to every
Dockerfileby default.
4. A minimal multi-stage Dockerfile
Multi-stage builds keep toolchains out of the runtime image.
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node
CMD [- The
buildstage carries dev dependencies; the final stage does not. COPY --from=buildmoves only the artifacts you need.USER nodedrops root before the process starts.
5. Pin the base image
Pin by tag at minimum, by digest for reproducibility.
FROM node:22-slim@sha256:2b9f...c31a- A digest is immutable, so the same line builds the same base forever.
- Tags like
latestdrift and break reproducibility. - Internal registries let the org govern which digests are approved.
6. Expose config through the environment
Read configuration from the environment, not from files baked into the image.
ENV NODE_ENV=production
EXPOSE 8080EXPOSEdocuments the port; it does not publish it.- Runtime values come from Kubernetes env vars or a Secret, not the image.
- The same image runs in every environment - only config changes.
7. A local Compose file
Compose v2 wires a service and its dependencies for the inner loop.
services:
api:
build: ./services/api
ports:
- "8080:8080"
depends_on:
- db
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: devbuild:points at the service context, reusing the sameDockerfile.depends_onorders startup for local development.- This file is a dev tool; it does not define your production deployment.
Intermediate Examples
8. Repo root layout for many services
Group deployables and shared code so build contexts stay predictable.
repo/
services/
api/Dockerfile
worker/Dockerfile
packages/
shared/
.dockerignore
compose.yaml
Makefile- Services live under
services/, shared libraries underpackages/. - A single top-level
.dockerignoregoverns root-context builds. - A
Makefile(orjust) standardizesbuild,scan, andpush.
9. A build target wrapper
Wrap the build so every developer and CI job runs the same command.
IMAGE ?= registry.internal/api
TAG ?= $(shell git rev-parse --short HEAD)
build:
docker build -f services/api/Dockerfile -t $(IMAGE):$(TAG) .- The context is the repo root (
.) so shared packages are reachable. - The tag is the git SHA, making images traceable to a commit.
- CI and laptops call
make build, so behavior does not drift.
10. Bind-mount for a fast inner loop
Mount source into a running container to iterate without rebuilding.
services:
api:
build:
context: ./services/api
target: build
command: npm run dev
volumes:
- ./services/api/src:/app/srctarget: buildstops at the dev stage that has the toolchain.- The bind mount reflects local edits instantly.
- Production still ships the slim final stage, unaffected by this dev config.
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).