Integrating ESLint & Prettier in DevContainers

Introduction

Establishing deterministic code quality standards requires isolating linting and formatting dependencies from host machine variations. This guide outlines a minimal, reproducible configuration for containerized JavaScript and TypeScript workflows, aligning with Customization & Developer Toolchain Integration standards.

1. Base Image Selection & Dependency Pinning

Start with an official Microsoft DevContainer base image to guarantee OS-level consistency. Pin Node.js versions explicitly in the Dockerfile or devcontainer.json to prevent drift. Avoid floating tags like latest or current. Explicit versioning ensures that every developer and CI runner executes identical binary paths, eliminating silent failures caused by upstream package manager updates.

2. Post-Creation Hook & Dependency Installation

Use postCreateCommand to execute package installation inside the container lifecycle. Always prefer npm ci over npm install in automated hooks — the lockfile-based installation guarantees deterministic dependency trees and prevents unexpected resolution conflicts during container initialization.

For teams standardizing global configurations, refer to Automating Dotfiles Sync Across Containers to propagate shared linting presets.

3. VS Code Extension Mapping & Workspace Settings

Declare dbaeumer.vscode-eslint and esbenp.prettier-vscode in the customizations.vscode.extensions array. Configure .vscode/settings.json to delegate formatting exclusively to Prettier while routing linting to ESLint. This setup directly supports Pre-commit Hook Configuration for Containerized Workflows by ensuring local execution matches CI validation.

Explicitly defining editor.defaultFormatter prevents VS Code from prompting users to select a formatter on first launch, enforcing zero-friction onboarding for new contributors.

4. Rule Conflict Resolution & Cache Management

Install eslint-config-prettier to disable ESLint formatting rules that conflict with Prettier. Mount node_modules/.cache as a named volume to persist the ESLint cache across container rebuilds. Without explicit cache mapping, the container filesystem treats every lint invocation as a fresh scan. Persisting the cache directory across lifecycle events maintains sub-second feedback loops during iterative development.

Code

devcontainer.json

{
  "name": "Node.js & ESLint/Prettier",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm",
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode"
      ],
      "settings": {
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "editor.formatOnSave": true
      }
    }
  },
  "mounts": [
    "source=eslint-cache,target=/workspace/node_modules/.cache,type=volume"
  ],
  "postCreateCommand": "npm ci"
}

.vscode/settings.json

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "eslint.validate": [
    "javascript",
    "typescript"
  ],
  "prettier.requireConfig": true
}

package.json

{
  "devDependencies": {
    "eslint": "^8.57.0",
    "prettier": "^3.2.0",
    "eslint-config-prettier": "^9.1.0"
  },
  "scripts": {
    "lint": "eslint . --ext .js,.ts",
    "lint:fix": "eslint . --ext .js,.ts --fix",
    "format": "prettier --write ."
  }
}

.eslintrc.js

module.exports = {
  extends: [
    'eslint:recommended',
    'prettier'
  ],
  env: {
    node: true,
    es2022: true
  }
};

Common Pitfalls

  • Installing ESLint/Prettier globally in the container instead of as project devDependencies causes version skew across workspaces.
  • Omitting eslint-config-prettier results in conflicting formatting rules and infinite editor save loops.
  • Failing to mount the ESLint cache directory leads to repeated full-tree lint scans on every container restart.
  • Relying on host machine Node versions instead of container-pinned binaries breaks CI parity.
  • Running npm run lint:fix inside postCreateCommand on an empty workspace fails if no source files exist yet; gate it with an existence check.

Conclusion

The key insight is that the devcontainer.json should own extension declarations while package.json owns the exact tool versions. This split means any developer who opens the repo gets the same ESLint/Prettier versions as CI, with no host-level configuration required.

FAQ

How do I prevent ESLint and Prettier from conflicting on formatting rules? Install eslint-config-prettier and add it as the last entry in the extends array in your ESLint config. This disables all ESLint rules that overlap with Prettier, delegating formatting exclusively to Prettier.

Why does my containerized ESLint run slower than the host version? Container filesystems often lack persistent caching. Mount the node_modules/.cache directory to a Docker named volume to persist the ESLint cache between rebuilds, reducing cold-start scan times significantly.

Should I install ESLint/Prettier globally in the DevContainer? No. Always install them as devDependencies in package.json. Global installations bypass project-specific version locks and break deterministic CI parity when different projects require different rule sets.