Docker Development Workflow — Compose Monorepo, MongoDB Seeding, and Dev Containers

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
Note: VS Code Dev Containers (defined in .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.
Tip: Add a 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.
Warning: The .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”

🧠 Test Yourself

A developer adds a new npm package by running npm install express-validator on their host machine (macOS), then runs docker compose up. The container starts, but loading the module fails with a native module error. What is the cause and correct fix?