Inspired by best practices from talented practitioners like Brett Fisher, whose guidance I’ve followed for over a decade.
This repository demonstrates how I package and run NestJS applications with Docker using opinionated defaults that balance simplicity, performance, and maintainability.
Most examples online either stop at a Dockerfile for local testing or drown you in Kubernetes/CI/CD specifics. This repo focuses on the middle ground: good defaults that actually work for production microservices, without overengineering.
- Real-world context. These defaults are based on proven best practices for production-ready Node.js services using NestJS.
- Optimized for DX and Ops. Simple to build, secure, and predictable images suitable for production.
- Patterns, not snippets. You can reuse this structure across projects, not just copy/paste a Dockerfile.
- Security first. Use node:22-alpine (current LTS, small, maintained, minimal attack surface).
- Multi-stage builds. Keep images small by separating build and runtime stages.
- Reproducibility. Deterministic installs (npm ci / pnpm install --frozen-lockfile).
- Non-root runtime. Containers drop privileges → run as unprivileged user.
- Operational hygiene. Containers provide health checks and log cleanly.
This template provides two main components that work together to create a production-ready NestJS application:
Production-ready containerization with security and performance best practices:
- Multi-stage Dockerfile → build stage with all dependencies, runtime stage with only what's needed
- External healthcheck script for container health monitoring
- Security hardening with non-root user, minimal attack surface, and cleanup procedures
- Build arguments for Node version, version metadata, and build date customization
- Graceful shutdown with proper signal handling
- Optimized for production with minimal attack surface and reproducible builds
Enterprise-grade development workflow that balances strictness with developer experience:
- ESLint configuration with TypeScript rules, production vs test environment handling
- Prettier formatting with TypeScript-specific indentation (4 spaces) and 120-character line length
- Husky Git hooks for pre-commit linting and pre-push coverage validation
- lint-staged for automated code quality checks on staged files only
- Coverage thresholds (80%) enforced in Jest, pre-push hooks, and CI pipeline
- Unit test isolation with proper mocking and test structure
- GitHub Actions CI workflow with branch-specific strategies for automated quality gates
Build the production image with default settings:
docker build -t my-nest-service:latest .
Build with a specific Node.js version:
docker build --build-arg NODE_VERSION=20 -t my-nest-service:latest .
Build with custom version and timestamp:
docker build \
--build-arg VERSION=1.0.0 \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
-t my-nest-service:1.0.0 .
docker run --rm -p 3000:3000 my-nest-service:latest
The container includes an external health check script that runs every 30 seconds:
docker run --rm -p 3000:3000 my-nest-service:latest
# Run linting
npm run lint
# Check formatting
npm run format:check
# Fix formatting
npm run format
# Run tests with coverage
npm run test:cov
- Pre-commit: Runs ESLint and Prettier on staged files
- Pre-push: Runs full test suite with coverage validation
- Feature branches: Automatic lint + build validation on pull requests
- Main branch: Full CI with tests, coverage, and security audits
- Coverage enforcement: 80% threshold required for all code
Customize coverage requirements in package.json
:
"jest": {
"coverageThreshold": {
"global": {
"branches": 80, // Branch coverage percentage
"functions": 80, // Function coverage percentage
"lines": 80, // Line coverage percentage
"statements": 80 // Statement coverage percentage
}
}
}
Git hooks are automatically configured. To customize:
Pre-commit hook (.husky/pre-commit
):
npm run lint:staged
Pre-push hook (.husky/pre-push
):
npm run test:cov # Runs tests with coverage validation
Modify rules in eslint.config.mjs
:
- Production code: Strict TypeScript rules with warnings for best practices
- Test files: Relaxed rules for testing flexibility
- Line length: 120 characters (configurable in
max-len
rule)
Customize formatting in .prettierrc
:
{
"singleQuote": true,
"trailingComma": "es5",
"arrowParens": "avoid",
"printWidth": 120,
"overrides": [
{
"files": "*.ts",
"options": { "tabWidth": 4 }
}
]
}
Modify .github/workflows/ci.yml
:
- Node version: Change
node-version: '22'
to your preferred version - Coverage thresholds: Adjust Jest configuration (see above)
- Job conditions: Modify
if
conditions for different branch behaviors
NestJS is a powerful framework, but without solid packaging and deployment practices, you end up with bloated, brittle containers. This repo is my baseline “good defaults” setup: not perfect, but safe, lean, and production-friendly out of the box.
Score: 9.7 / 10
Strengths: Security hardening, clean architecture, attack surface reduction, full documentation
Minor: Consider Alpine version pinning
Verdict: Production-ready, enterprise-hardened, passes security audits
Score: 9.5 / 10
Strengths: Comprehensive ESLint/Prettier setup, automated Git hooks, coverage enforcement, CI/CD integration
Features: TypeScript-specific formatting, branch-specific CI strategies, security audits
Verdict: Enterprise-grade development workflow with automated quality gates
Production-Ready Docker for NestJS: Good Defaults That Actually Work ➡️ Read the full article
This article walks through the Dockerfile step by step and explains the reasoning behind each decision (multi-stage builds, non-root runtime, reproducible installs, health checks, etc.).
Code Quality & Developer Experience in NestJS: How Not to Kill Your Team ➡️ Read the full article
This article explains the philosophy behind the code quality setup in this template. It covers why developer experience matters more than strict rules, how to balance code quality with team productivity, and the practical implementation of ESLint, Prettier, Husky, and CI/CD integration.
Key insights from the article:
- Why code quality without DX is a treadmill that burns out teams
- How to enforce practical rules that matter, not style snobbery
- The importance of team-wide synchronization and onboarding
- Balanced TypeScript rules for production vs test environments
- Automated quality gates that don't slow down development