✦ For everyone, free.

Practical knowledge for real and everyday life

Home

14.2.1.1 Dev Environment Config

A focused guide to Dev Environment Config, connecting core concepts with practical Docker and container operations.

Development environment configuration for Docker concerns how containers are set up to maximize iteration speed and debuggability on a developer's machine, which is a deliberately different set of priorities from production configuration, where stability, security, and resource discipline take precedence over convenience.

Optimizing for fast iteration

The defining characteristic of a development configuration is that code changes should be reflected immediately, without rebuilding an image for every edit. Bind-mounting the source tree into the container, rather than copying it in at build time, achieves this directly:

docker run -v "$(pwd)":/app -p 3000:3000 my-api:dev
services:
  api:
    build: .
    volumes:
      - .:/app
      - /app/node_modules
    ports:
      - "3000:3000"

The second, anonymous volume entry for /app/node_modules is a common pattern: it prevents the host bind mount from shadowing dependencies that were installed inside the image during the build, which would otherwise be hidden behind an empty or host-specific node_modules directory.

Hot reload and watch tooling

Pairing the bind mount with a file-watching process inside the container lets changes take effect without even restarting the container:

FROM node:20
WORKDIR /app
COPY package*.json .
RUN npm install
COPY . .
CMD ["npx", "nodemon", "server.js"]
docker compose watch

Compose's watch feature (where supported) can synchronize specific file changes into a running container or trigger a rebuild automatically, reducing the manual rebuild-and-restart cycle further.

A separate Dockerfile stage or file for development

Rather than forcing one Dockerfile to serve both development and production needs awkwardly, a multi-stage build or a dedicated development-only Dockerfile keeps each environment's concerns isolated:

FROM node:20 AS base
WORKDIR /app
COPY package*.json .
RUN npm install

FROM base AS development
CMD ["npx", "nodemon", "server.js"]

FROM base AS production
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]
docker build --target development -t my-api:dev .

Development-only services

A development Compose file commonly includes services that have no place in production, such as a mail-catching SMTP server, a database administration UI, or a mock external API:

services:
  mailhog:
    image: mailhog/mailhog
    ports:
      - "8025:8025"
  adminer:
    image: adminer
    ports:
      - "8080:8080"

Keeping these in a separate docker-compose.override.yml (Compose's default override filename, applied automatically alongside the base file) means a developer gets useful tooling for free without that tooling ever being a candidate for accidental inclusion in a production stack.

Debug ports and tooling

Development containers often need to expose a debugger port that a production container never should:

docker run -p 9229:9229 -e NODE_OPTIONS="--inspect=0.0.0.0:9229" my-api:dev
{
  "type": "node",
  "request": "attach",
  "address": "localhost",
  "port": 9229,
  "localRoot": "${workspaceFolder}",
  "remoteRoot": "/app"
}

This configuration lets an IDE attach a debugger directly to the process running inside the container, stepping through code exactly as it would for a process running natively on the host.

Relaxed resource limits and verbose logging

Development containers generally benefit from looser resource constraints and noisier logging than production, since the goal is visibility and unblocked iteration rather than efficient resource usage:

services:
  api:
    environment:
      - LOG_LEVEL=debug
    # no memory or CPU limits set in development

Seeding and resetting state quickly

Because development data is disposable, a development configuration typically favors easily destroyable, quickly reseedable state over the careful, backed-up persistence that production requires:

docker compose down -v
docker compose up -d
docker compose exec api npm run db:seed

Tearing down volumes entirely and reseeding from a fixture script is a normal, frequent part of a development workflow in a way that would be unacceptable for a production volume.

Keeping development and production from silently diverging

The risk of a heavily customized development configuration is that it diverges so far from production that issues only ever surface after deployment. A development setup should still build from the same base image and the same Dockerfile stages as production, differing only in the specific overrides layered on top, which keeps the two environments aligned at the level that matters most.

docker build --target production -t my-api:prod-test .
docker run --rm my-api:prod-test

Periodically running the production-target build locally, even during development, catches divergence early rather than at deploy time.

Common mistakes

  • Bind-mounting the entire source tree without excluding dependency directories, leading to confusing failures when host and container dependency versions differ.
  • Letting a development Dockerfile and a production Dockerfile diverge so far that they no longer share a meaningful base, undermining confidence that what is tested locally reflects what ships.
  • Leaving debug ports or development-only services defined in the same Compose file used for production deployment, relying on operators to remember to omit them manually.
  • Applying production-style resource limits in development, slowing down iteration without any corresponding benefit, since a developer's machine is not subject to multi-tenant resource pressure the way a production host is.
  • Treating development database volumes as precious, when in practice fast, frequent destruction and reseeding is usually the more productive workflow.

A good development environment configuration for Docker prioritizes fast feedback loops, easy resets, and debugging access, while still sharing enough of its build definition with the production configuration that the two never drift far enough apart to hide real issues until deployment.