A Docker-based development workflow reduces onboarding from days to minutes — a new developer clones the repo, runs docker compose up, and has the complete MEAN Stack running with hot reload, a seeded database, and all infrastructure dependencies. This lesson assembles the complete development workflow: project structure, environment variable management, MongoDB seeding, VS Code DevContainer integration, and the common day-to-day Docker commands that make working with containers as smooth as working directly on the host.
Docker Development Workflow Commands
| Task | Command |
|---|---|
| First-time setup | cp .env.example .env && docker compose up -d --build |
| Daily startup | docker compose up -d |
| View all logs | docker compose logs -f |
| View API logs only | docker compose logs -f api |
| Restart after code change | Automatic (nodemon / ng serve with hot reload) |
| Install new npm package | docker compose exec api npm install new-package |
| Run tests | docker compose exec api npm test |
| Access MongoDB shell | docker compose exec mongodb mongosh -u admin -p secret |
| Access Redis CLI | docker compose exec redis redis-cli -a redispass |
| Reset database | docker compose down -v && docker compose up -d |
| Rebuild single service | docker compose up -d --build api |
.devcontainer/devcontainer.json) take Docker development one step further — VS Code itself runs inside a container, with the correct Node.js version, all extensions, and debugging configured automatically. When a team shares a devcontainer.json, every developer has the exact same editor environment. This eliminates “ESLint version conflicts” and “my debugger doesn’t work” issues that arise from differences in local VS Code extension versions.Makefile with aliases for common Docker Compose commands. Typing make up instead of docker compose up -d --build reduces friction and ensures consistent usage across the team. Common targets: up, down, logs, test, seed, shell-api, shell-mongo. A well-documented Makefile doubles as runbook documentation — make help can list all available targets with descriptions..env file must never be committed to version control — it contains secrets. Add .env to .gitignore. Commit a .env.example file with all the required variable names but placeholder values. New team members copy this to .env and fill in actual values. If .env is ever accidentally committed, rotate all secrets immediately — assume they are compromised, as Git history is public even if the commit is later removed.Complete Development Setup
# Project structure for Docker-based MEAN Stack monorepo
taskmanager/
├── api/ # Express API
│ ├── src/
│ ├── Dockerfile
│ ├── .dockerignore
│ ├── .env.example
│ └── package.json
├── client/ # Angular app
│ ├── src/
│ ├── Dockerfile
│ ├── .dockerignore
│ ├── nginx.conf
│ └── package.json
├── mongo-init.js # MongoDB initialisation script
├── nginx/
│ ├── dev.conf
│ └── prod.conf
├── .devcontainer/
│ └── devcontainer.json # VS Code Dev Container config
├── docker-compose.yml # Development
├── docker-compose.prod.yml # Production overrides
├── .env.example # Variable template (committed)
├── .env # Actual values (gitignored)
├── Makefile # Convenience commands
└── README.md
// mongo-init.js — runs on first container start when data volume is empty
// Mounted as: ./mongo-init.js:/docker-entrypoint-initdb.d/init.js
db = db.getSiblingDB('taskmanager');
db.createUser({
user: 'taskapp',
pwd: 'taskapppass',
roles: [{ role: 'readWrite', db: 'taskmanager' }],
});
// Create indexes
db.users.createIndex({ email: 1 }, { unique: true });
db.tasks.createIndex({ user: 1, createdAt: -1 });
db.tasks.createIndex({ title: 'text', description: 'text' });
// Seed demo data
db.users.insertOne({
_id: ObjectId('64a1f2b3c8e4d5f6a7b8c9d1'),
name: 'Demo User',
email: 'demo@taskmanager.io',
password: '$2b$12$demoHashedPasswordForDevelopment....',
role: 'admin',
isVerified: true,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
});
db.tasks.insertMany([
{
title: 'Explore the Task Manager',
status: 'pending',
priority: 'medium',
user: ObjectId('64a1f2b3c8e4d5f6a7b8c9d1'),
tags: ['demo', 'getting-started'],
createdAt: new Date(),
updatedAt: new Date(),
},
{
title: 'Read the documentation',
status: 'completed',
priority: 'low',
user: ObjectId('64a1f2b3c8e4d5f6a7b8c9d1'),
tags: ['demo'],
createdAt: new Date(),
updatedAt: new Date(),
},
]);
print('MongoDB initialisation complete');
// .devcontainer/devcontainer.json — VS Code Dev Containers
{
"name": "MEAN Stack Dev Container",
"dockerComposeFile": ["../docker-compose.yml"],
"service": "api",
"workspaceFolder": "/app",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"mongodb.mongodb-vscode",
"humao.rest-client",
"ms-azuretools.vscode-docker",
"angular.ng-template"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter":"esbenp.prettier-vscode",
"eslint.workingDirectories": ["./api", "./client"]
}
}
},
"forwardPorts": [3000, 4200, 27017],
"postStartCommand": "echo 'Dev container ready'"
}
# Makefile — convenience wrappers for Docker Compose commands
.PHONY: up down logs test seed shell-api shell-mongo help
up: ## Start all services (detached)
docker compose up -d
build: ## Rebuild images and start
docker compose up -d --build
down: ## Stop all services
docker compose down
clean: ## Stop all services and remove volumes (DELETES DATA)
docker compose down -v
logs: ## Follow all service logs
docker compose logs -f
logs-api: ## Follow API logs only
docker compose logs -f api
test: ## Run API tests
docker compose exec api npm test
seed: ## Reset and re-seed database
docker compose down -v && docker compose up -d
shell-api: ## Open shell in API container
docker compose exec api sh
shell-mongo: ## Open MongoDB shell
docker compose exec mongodb mongosh -u admin -p secret taskmanager
install: ## Install a new npm package (usage: make install PACKAGE=express-validator)
docker compose exec api npm install $(PACKAGE)
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
How It Works
Step 1 — mongo-init.js Runs Once on First Volume Initialisation
Files placed in /docker-entrypoint-initdb.d/ inside the MongoDB container run when the data volume is empty (first startup). The init script creates the application user, sets up indexes, and seeds demo data. On subsequent startups, the volume already has data so the init script is not executed. This provides zero-configuration database setup — a developer running for the first time gets a seeded database automatically.
Step 2 — .env.example Documents Required Variables
The .env.example file is the contract for what environment variables the application needs. It is committed to git and contains all variable names with placeholder values and comments explaining each one. New developers copy it to .env and fill in actual values. In CI/CD, variables are injected by the pipeline rather than from a file. The separation between the template (committed) and the actual values (gitignored) is the key pattern.
Step 3 — Dev Containers Standardise the Developer Environment
VS Code Dev Containers run the editor inside a Docker container. When a developer opens the project in VS Code, it detects the .devcontainer/devcontainer.json, builds (or pulls) the container, and reopens VS Code with the editor running inside it. All extensions are automatically installed, all settings are applied, and all ports are forwarded. Every developer on the team has an identical environment — eliminating “works on my machine” debugging at the editor level.
Step 4 — Makefile Aliases Reduce Cognitive Load
A Makefile turns multi-part Docker Compose commands into single memorable words. make up is easier to remember and type than docker compose up -d, and much better than copy-pasting from a README. The help target (using grep to parse ## comments) generates self-documenting help text — make help lists all available commands with their descriptions, making the Makefile discoverable without reading it.
Step 5 — Hot Reload Works Through Bind Mounts + CHOKIDAR_USEPOLLING
The bind mount ./api:/app exposes the host filesystem inside the container. When nodemon detects a file change (via polling, because inotify events don’t propagate from macOS/Windows hosts), it restarts the Node.js process inside the container. The change is immediate — saving a file in VS Code reflects in the running container within a second, just as it would when running locally without Docker.
Common Mistakes
Mistake 1 — Committing .env to git
❌ Wrong — credentials in git history are permanent even after deletion:
git add .env # JWT_SECRET, MONGO_ROOT_PASS now in git history forever!
git commit -m "add env"
✅ Correct — .env in .gitignore, .env.example committed:
# .gitignore:
.env
.env.local
.env.*.local
# Committed:
.env.example # variable names + placeholder values only
Mistake 2 — npm installing on the host then rebuilding
❌ Wrong — host node_modules may conflict with container’s:
npm install new-package # installs on host (macOS)
docker compose up --build # container has both host (macOS) and container (Linux) modules mixed
✅ Correct — install inside the running container:
docker compose exec api npm install new-package
# Installs inside Linux container — correct platform, immediately available
Mistake 3 — Not documenting the Docker setup in README
❌ Wrong — new developer spends hours figuring out how to start the app:
# README.md has no Docker setup instructions
# Developer tries: npm install, npm start, node server.js — all fail
✅ Correct — clear Getting Started section:
# README.md:
# Getting Started:
# 1. cp .env.example .env
# 2. docker compose up -d --build
# 3. Open http://localhost:4200
Quick Reference
| Task | Command |
|---|---|
| First setup | cp .env.example .env && docker compose up -d --build |
| Install npm package | docker compose exec api npm install pkg |
| Run migrations/seeds | docker compose exec api npm run seed |
| Open mongo shell | docker compose exec mongodb mongosh -u admin -p pass |
| Debug API in VS Code | Add --inspect=0.0.0.0:9229 to nodemon command + VS Code launch.json |
| Reset everything | docker compose down -v && docker compose up -d |
| Check service health | docker compose ps — shows health status |
| Dev Containers | Open repo in VS Code → “Reopen in Container” |