Standards for building, tagging, and running container images in DevRail-managed repositories. These complement the CI/CD Pipelines build stage and the Universal Security Tools scanning.
- Use official or verified images. Pull from Docker Hub official images, verified publishers, or your organization's private registry. Never use unverified community images for production workloads.
- Prefer minimal base images. Use
alpine,distroless, or-slimvariants when the application supports them. Smaller images mean fewer vulnerabilities and faster pulls. - Match the runtime to the application. Use language-specific runtime images (
python:3.12-slim,node:20-alpine) rather than installing runtimes on a generic base.
| Approach | When to Use | Example |
|---|---|---|
| Pin to digest | Production builds, security-critical images | python@sha256:abc123... |
| Pin to specific tag | General development | python:3.12.3-slim-bookworm |
Never use latest |
-- | python:latest is prohibited |
Pin base images to a specific version or digest. Unversioned tags (latest, stable) can change without notice and break reproducibility.
Use multi-stage builds to separate build dependencies from the runtime image:
# Build stage
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Runtime stage
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /app .
USER appuser
CMD ["python", "main.py"]- Build tools stay in the build stage. Compilers, package managers, header files -- none of these belong in the runtime image.
- Copy only what is needed. Use
COPY --from=builderto bring over only application code and installed dependencies. - The runtime image should not have
pip,npm,apt, or equivalent. If the application does not need them at runtime, do not include them.
Order Dockerfile instructions from least-changing to most-changing:
1. Base image (FROM)
2. System packages (apt-get, apk)
3. Application dependencies (requirements.txt, package.json)
4. Application code (COPY . .)
5. Runtime configuration (CMD, ENTRYPOINT)
This maximizes cache reuse. Changing application code does not invalidate the dependency installation layer.
COPYdependency manifests before application code. Copyrequirements.txt,package.json,go.sumfirst, install dependencies, then copy the rest of the application.- Combine
RUNcommands where logical. Reduce layers by chaining related commands:RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* - Clean up in the same layer. Package manager caches (
/var/lib/apt/lists/*,pip cache) must be removed in the sameRUNinstruction that creates them.
-
Never run containers as root in production. Create a dedicated user and switch to it:
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser USER appuser
-
Set
USERafter allRUNcommands that require root (package installation, directory creation). -
Verify with
docker run --rm <image> whoami. The output should not beroot.
-
Never use
ENVorARGfor secrets. These are visible in image history (docker history). -
Use BuildKit
--secretmounts for build-time secrets:RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm install -
Inject runtime secrets via environment variables or mounted volumes. The image itself contains no credentials.
- Scan images with
trivyas part ofmake scan. This is enforced in CI. - Fix or accept findings before release. Critical and high vulnerabilities must be addressed. Document accepted risks for lower severities.
- Rebuild images when base images receive security updates. Monitor base image advisories and rebuild regularly.
| Context | Tag Format | Example |
|---|---|---|
| Release | vX.Y.Z |
myapp:v1.2.0 |
| CI build | sha-<short> |
myapp:sha-a1b2c3d |
| Branch build | branch-<name> |
myapp:branch-feat-auth |
- Never overwrite a release tag.
v1.2.0always points to the same image. If the image is defective, releasev1.2.1. - Never use
latestfor production deployments. Use explicit version tags. latestmay be updated as a convenience for development, pointing to the most recent release. It is never the source of truth.- Include the image digest in deployment manifests when maximum reproducibility is required.
Include a HEALTHCHECK instruction for standalone containers:
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/healthz || exit 1When running under Kubernetes or similar orchestrators, prefer orchestrator-level probes over HEALTHCHECK:
| Probe | Purpose | Endpoint |
|---|---|---|
| Liveness | Is the process alive? Restart if not. | /healthz |
| Readiness | Can the process serve traffic? Remove from load balancer if not. | /readyz |
| Startup | Has the process finished initializing? | /healthz with extended timeout |
- Every service container exposes a health endpoint. No exceptions.
- Health checks do not test external dependencies.
/healthzchecks the process itself, not the database or upstream APIs. Use/readyzfor dependency checks. - Health checks are fast. Target < 100ms response time. No heavy computation or I/O.
Every project with a Dockerfile must have a .dockerignore file. This prevents unnecessary files from entering the build context.
.git
.gitignore
.env
.env.*
*.md
LICENSE
Makefile
docker-compose*.yml
.github/
.gitlab-ci.yml
tests/
docs/
__pycache__/
*.pyc
.terraform/
*.tfstate
node_modules/
.venv/
- Mirror
.gitignorepatterns and add build-specific exclusions. - Exclude test and documentation directories unless the application needs them at runtime.
- Exclude CI configuration files.
.github/,.gitlab-ci.yml, and similar files are not needed in the image.
- The
ghcr.io/devrail-dev/dev-toolchain:v1image follows all of these standards and serves as a reference implementation. - For container orchestration standards (Kubernetes manifests, Helm charts, docker-compose), refer to the deployment-specific documentation for your project.
- Container builds should be reproducible. Given the same source code, lock files, and base image digest, the build should produce a functionally identical image.