Skip to content

Docker

Docker is the standard packaging format for our services. Containerizing applications keeps local development, CI, and deployed environments aligned and reduces drift between machines.

Docker helps us:

  • Run services with the same runtime and system dependencies in every environment
  • Make onboarding faster by reducing machine-specific setup
  • Package applications into repeatable deployable artifacts
  • Keep supporting services such as databases, caches, and workers easy to start locally

Each service repository should provide:

  • A Dockerfile for building the application image
  • A compose.yml file for local development when the service depends on other containers
  • A .dockerignore file to keep build contexts small
  • Environment variables for configuration instead of hard-coded values
  • A documented startup command

If a service does not need supporting infrastructure locally, a Dockerfile alone may be sufficient.

Dockerfiles should be written to be predictable, secure, and easy to cache.

Recommended practices:

  • Use a small, well-supported base image, preferably alpine
  • Pin runtime versions explicitly instead of relying on moving tags such as latest
  • Prefer multi-stage builds when compilation or bundling is required
  • Copy dependency manifests before application source to improve layer caching
  • Run the application as a non-root user when practical
  • Keep images focused on one process or responsibility
  • Pass configuration through environment variables
  • Avoid baking secrets into the image
Dockerfile
FROM <runtime-image>:<pinned-version> AS build
WORKDIR /app
COPY <dependency-manifests> ./
RUN <install-dependencies>
COPY . .
RUN <build-command>
FROM <runtime-image>:<pinned-version> AS runtime
WORKDIR /app
ENV APP_ENV=production
COPY --from=build /app/<build-output> ./<build-output>
COPY --from=build /app/<runtime-assets> ./<runtime-assets>
USER <non-root-user>
EXPOSE <port>
CMD ["<start-command>"]

This pattern applies across stacks, but the exact dependency files, build outputs, and startup commands vary by language and framework.

Use compose.yml to define the full local development stack when a service depends on infrastructure such as:

  • Databases
  • Caches
  • Queues
  • Background workers
  • Mock or supporting services

Keep local Compose configurations focused on development ergonomics:

  • Mount source code only when live reload or iterative local development requires it
  • Use named volumes for persistent service state when useful
  • Prefer explicit port mappings
  • Keep service names short and descriptive
  • Avoid container_name unless there is a clear operational need
  • Document any required environment variables in the repository README.md
compose.yml
services:
app:
build:
context: .
ports:
- "<host-port>:<container-port>"
env_file:
- .env
depends_on:
- data
data:
image: <service-image>:<pinned-version>
ports:
- "<host-port>:<container-port>"
environment:
<service-setting>: <value>
volumes:
- data-volume:/var/lib/<service>
volumes:
data-volume:

This pattern keeps the application container and its supporting services in one place without assuming a specific language, database, or framework.

Start the stack with:

Terminal window
docker compose up --build

Stop it with:

Terminal window
docker compose down

If you need to remove volumes as well:

Terminal window
docker compose down --volumes

Container images should be reusable across environments. Environment-specific configuration should be injected at runtime.

Use environment variables for:

  • Database connection strings
  • API keys
  • Service URLs
  • Feature flags
  • Runtime mode settings

Do not:

  • Commit secrets to the repository
  • Bake credentials into Dockerfiles
  • Depend on local-only machine paths inside containers

Keep images small and understandable.

Prefer to:

  • Exclude unnecessary files with .dockerignore
  • Install only the dependencies required for the target environment
  • Remove temporary build artifacts in the same layer when applicable
  • Use clear tags in CI and deployment pipelines

Common .dockerignore entries include:

.dockerignore
.git
.gitignore
.DS_Store
.idea
.vscode
node_modules
dist
build
coverage
.env
.env.*
*.log
tmp

The right .dockerignore file depends on the stack. Include generated assets, dependency directories, local environment files, and editor or OS artifacts that should never be copied into the build context.

The standards on this page apply broadly, but service-level details vary. Exact runtime images, dependency files, build outputs, health checks, and startup commands should be documented in the repository that owns the service.

Examples:

  • JavaScript or TypeScript services may copy package.json, lockfiles, and compiled output
  • Go services may compile a binary in a builder stage and copy only the binary into the final image
  • Frameworks such as Astro, Next.js, or API platforms may require different build directories and runtime assets

When using Docker in application repositories:

  • Expose only the ports the service actually uses
  • Add health checks where orchestration or dependent services need readiness information
  • Keep logs written to standard output and standard error
  • Treat containers as replaceable runtime units, not long-lived mutable servers

Docker improves consistency, but it does not replace good application defaults, clear documentation, or disciplined dependency management.