Dockerfile Best Practices
All applications at DIT must be containerized using Docker. This document provides comprehensive guidelines for writing secure, efficient, and maintainable Dockerfiles.
Base Image Requirements
Use DIT's Internal Registry
Base images should be pulled from DIT's on-premise container registry (reg.dev.krd) when available:
# ✅ Preferred - Using DIT's registry
FROM reg.dev.krd/dotnet/aspnet:10-noble
# ✅ Also OK - Public registry when not available internally
FROM node:22-alpineWhy use DIT's registry
- Ensures availability if external registries are unreachable
- Images are scanned for vulnerabilities
If a commonly used image is not available in the internal registry, request it from the DevOps Team to be mirrored.
Pin Image Versions
Use specific version tags for application base images:
# ✅ Correct - Pinned version for application base
FROM reg.dev.krd/library/python:3.12-slim
# ❌ Wrong - Unpinned version for application base
FROM reg.dev.krd/library/python:latestWhen latest is acceptable
Using latest is acceptable for utility stages that don't affect the final image, such as extracting CA certificates:
# ✅ OK - Utility stage for extracting certs
FROM alpine:latest AS certs
RUN apk --update add ca-certificatesThe key distinction is that utility stages are discarded after the build—only their artifacts are copied to the final image.
Prefer Minimal Base Images
Use the smallest base image that meets your requirements:
| Image Type | Use Case |
|---|---|
alpine | General-purpose, minimal footprint |
slim | Debian-based, smaller than full |
distroless | Production runtimes, no shell |
chiseled | .NET optimized, Ubuntu-based, no shell |
scratch | Static binaries (Go, Rust) |
For .NET applications, prefer chiseled images (aspnet:9.0-noble-chiseled) which:
- Have no shell or package manager
- Include only essential runtime dependencies
- Are significantly smaller than standard images
- Reduce attack surface
Multi-Stage Builds
Multi-stage builds are mandatory for all production images. They separate build-time dependencies from runtime, resulting in smaller and more secure images.
Example: Node.js Application
# Stage 1: Build
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# Stage 2: Production
FROM node:22-alpine AS production
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY package.json ./
# Use built-in 'node' user (UID 1000)
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]Layer Optimization
Docker builds images using a layered filesystem. Proper layer ordering dramatically improves build times through caching.
Order Instructions by Change Frequency
Place instructions that change least frequently at the top:
FROM node:22-alpine
# 1. System dependencies (rarely change)
RUN apk add --no-cache dumb-init
# 2. Package manager setup (rarely changes)
RUN corepack enable && corepack prepare pnpm@latest --activate
# 3. Dependency files (change occasionally)
COPY package.json pnpm-lock.yaml ./
# 4. Install dependencies (invalidated by step 3)
RUN pnpm install --frozen-lockfile --prod
# 5. Application code (changes frequently)
COPY . .
# 6. Metadata and runtime config (stable)
USER node
EXPOSE 3000
CMD ["node", "src/index.js"]Combine RUN Commands
Reduce layers by combining related commands:
# ✅ Good - Single layer
RUN apk add --no-cache \
curl \
git \
openssh-client \
&& rm -rf /var/cache/apk/*
# ❌ Bad - Multiple layers
RUN apk add --no-cache curl
RUN apk add --no-cache git
RUN apk add --no-cache openssh-clientSecurity Best Practices
Run as Non-Root User
All containers must run as non-root. This is enforced by the restricted Pod Security Standard.
Use Built-in Non-Root Users When Available
If the base image provides a non-root user, use it instead of creating a custom one. This is simpler and ensures compatibility with the base image.
| Base Image | Built-in User | UID |
|---|---|---|
node:*-alpine | node | 1000 |
aspnet:*-chiseled | (default non-root) | 1654 |
python:* | (none - create one) | - |
scratch | (none - create one) | - |
# ✅ Best - Use built-in user if available
USER node # Node.js images
# ✅ Good - Create user with UID 1001 if no built-in user exists
RUN addgroup -g 1001 -S appgroup \
&& adduser -u 1001 -S appuser -G appgroup
USER 1001
# ✅ For scratch images, create a passwd entry in the builder
RUN echo "appuser:x:1001:1001:appuser:/:" > /etc_passwdRecommended Custom UID: 1001
When creating a custom user, use UID 1001 for consistency across DIT applications. Any non-root UID will work, but 1001 is the recommended standard.
Standard ports for non-root applications:
| Port | Use Case |
|---|---|
8080 | HTTP APIs (Go, .NET, Java) |
3000 | Node.js / Frontend apps |
Use .dockerignore
Always include a .dockerignore file to exclude sensitive and unnecessary files:
# Version control
.git
.gitignore
# Dependencies (will be installed fresh)
node_modules
vendor
# Build outputs
dist
build
*.log
# IDE and editor files
.vscode
.idea
*.swp
# Secrets and environment files
.env
.env.*
*.pem
*.key
# Documentation
README.md
docs/
# Test files
__tests__
*.test.js
*.spec.js
coverage/Don't Store Secrets in Images
Secrets must never be baked into images:
# ❌ NEVER do this
ENV DATABASE_PASSWORD=secret123
COPY .env /app/.env
# ✅ Correct - Use runtime environment variables
# Secrets are injected at runtime via Kubernetes SecretsScan Images for Vulnerabilities
All images must pass vulnerability scanning before deployment. This is enforced automatically in CI/CD pipelines.
Health Checks
Do not include HEALTHCHECK instructions in Dockerfiles. Health checks are handled by Kubernetes liveness and readiness probes, which provide more flexibility and better integration with the orchestration layer.
Applications must expose separate probe endpoints under /probes/ as explained in the Kubernetes documentation for implementation details.
Liveness ≠ Readiness
Never use the same endpoint for both probes. The liveness probe should only check if the application process is alive, while the readiness probe should verify dependencies (database, cache, external services).
Common Patterns
Static File Server
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM reg.dev.krd/joseluisq/static-web-server:2 AS production
COPY --from=build /app/dist /public
ENV SERVER_PORT=8080
ENV SERVER_LOG_LEVEL=info
# Use built-in 'nobody' user provided by the base image
USER nobody
EXPOSE 8080.NET Application
FROM reg.dev.krd/dotnet/sdk:9.0-noble AS build
WORKDIR /src
# Copy project file and restore dependencies
COPY ["src/MyApi/MyApi.csproj", "MyApi/"]
RUN dotnet restore -r linux-x64 "MyApi/MyApi.csproj"
# Copy source code
COPY src .
WORKDIR "/src/MyApi"
# Publish the application
RUN dotnet publish "MyApi.csproj" --no-restore -r linux-x64 -c Release -o /app
# Use chiseled image for minimal attack surface
# Chiseled images run as UID 1654 by default (non-root)
FROM reg.dev.krd/dotnet/aspnet:9.0-noble-chiseled AS final
WORKDIR /app
COPY --from=build /app .
ENV DOTNET_EnableDiagnostics=0
ENV DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY=false
# Chiseled images already run as non-root (UID 1654)
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApi.dll"]Key points for .NET Dockerfiles
- Specify the linux-x64 runtime for smaller image size
- .NET chiseled images already run as non-root user (UID 1654). No additional user configuration is needed.
Go applications
FROM alpine:latest AS certs
RUN apk --update add ca-certificates
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-w -s' -o /go/bin/app \
&& echo "nobody:x:1001:1001:nobody:/:" > /etc_passwd
# Use scarch image (no deps required)
FROM scratch
COPY --from=builder /etc_passwd /etc_passwd
COPY --from=builder /go/bin/app /app
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
# Copy the dockerfile, go.mod, go.sum to support SBOM generation
COPY Dockerfile go.mod go.sum ./
USER 1001
EXPOSE 8080
ENTRYPOINT ["/app"]Key points for Go Dockerfiles
- Use
scratchbase image (empty, ~0MB overhead) CGO_ENABLED=0produces a static binary with no C dependencies-ldflags='-w -s'strips debug information, reducing binary size- Copy CA certificates for HTTPS/TLS connections
- Include
go.mod/go.sumfor Software Bill of Materials (SBOM) generation
Checklist
Before submitting a Dockerfile for review, verify:
- Base image is from
reg.dev.krd(preferred) or trusted public registry - Image version is pinned (except utility stages)
- Multi-stage build is used
- Runs as non-root user (built-in or UID 1001)
.dockerignoreexists and is comprehensive- No secrets or credentials in the image
- Layers are optimized for caching
- Application exposes
/probes/alive,/probes/ready, and/probes/startupendpoints
Following these best practices ensures that DIT container images are secure, efficient, and maintainable across all environments.
