Week 10 Demo 2 Docker
Instructor Overview¶
This lab builds a Docker-based development environment for students’ Node.js projects. The goal is a workflow where any teammate can clone the repo and be running with docker compose up — no setup doc required.
End state: Every student has:
- A
Dockerfilefor their Node.js app - A
docker-compose.ymlwith app + database - Bind mounts for live code reloading in dev
.env.exampleand.gitignoreupdated- (Stretch) Docker service wired into their Lab 1 CI workflow
Setup Checklist (Before Lab Starts)¶
Students should have:
- Docker Desktop installed and running (
docker run hello-worldworks) - Their Node.js project repo checked out locally
- Know what database their app uses (Postgres, MySQL, MongoDB, or none)
- Editor open
Instructor should have:
- Docker Desktop running
- Demo Node.js app ready (simple Express app with a DB connection works well)
- Terminal visible on projector
Part 1 — Confirm Docker Is Working (5 min)¶
Instructor script¶
“Before we write anything, let’s make sure everyone is in the same place.”
Expected output from hello-world:
“If you see that, you’re good. If you see an error, raise your hand now — we’ll fix it before moving on.”
Common fix for “permission denied” on Linux:
Common fix for “Docker Desktop not running”: Open Docker Desktop app, wait for whale icon in menu bar to stop animating.
Part 2 — Write the Dockerfile (15 min)¶
Start from scratch, explain every line¶
“We’re going to write a Dockerfile together. This tells Docker exactly how to build an image of your app.”
Type this live, pause and explain each instruction:
“We’re starting from an official Node.js image. Alpine is a minimal Linux — about 5MB vs 900MB for the full image. Same Node.js, much smaller.”
“All subsequent commands run from this directory inside the container. Think of it as
cd /appbut it also creates the directory.”
“Copy ONLY the package files first. We haven’t copied source code yet. Why? The next step is slow.”
“Install dependencies. This layer gets cached — it only re-runs if package.json changes. If we copied all our source code first, every code change would re-run this.”
“NOW copy the rest of the source code. This layer is fast to invalidate and rebuild.”
“Documents which port the app uses. Doesn’t actually open anything — that happens at
docker run.”
“The command that starts the app. Use the array form — it’s more reliable than a string.”
Full Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Build and test it¶
Walk through the output — point out each layer, show the caching indicator.
“Open localhost:3000 in your browser. Your app is running inside a container.”
Show the cache working:
“See how steps 1-4 say ‘CACHED’? Only the COPY and CMD steps re-ran. That’s the layer cache saving you time.”
Part 3 — Add .dockerignore (5 min)¶
“The most important line is
node_modules. Your Mac’s node_modules can’t run on Linux. We always install fresh inside the container withnpm ci. Without this, Docker copies 200MB of platform-specific binaries into the image and then npm ci overwrites them anyway.”
Show the difference:
# Check image size before .dockerignore
docker images myapp
# Add .dockerignore, rebuild
docker build -t myapp:dev .
docker images myapp
“Likely went from ~400MB to ~200MB just from excluding node_modules.”
Part 4 — Set Up Environment Variables (5 min)¶
Update .gitignore:
“
.env.exampleis your documentation — it tells teammates what variables the app needs without exposing real values..envhas the real values and never leaves your machine.”
Part 5 — Write docker-compose.yml (18 min)¶
“Running a single container is fine, but most apps need more than one process. Your app needs a database.
docker composelets us define the whole stack in one file.”
Build this up live:
“Specifies the Compose file format version.”
“The
appservice builds from the Dockerfile in the current directory.”
“Map port 3000 on your machine to port 3000 in the container. Left side is your machine, right side is the container.”
“Load environment variables from our .env file.”
“Two volumes here. The first mounts our local code into the container — any file you save locally immediately appears inside. The second is a trick: it tells Docker to use the container’s node_modules, not the one from your local mount.”
“Wait for the db service to start before starting the app.”
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
pgdata:
“The db service uses the official Postgres image — no Dockerfile needed. We persist the data in a named volume so the database survives container restarts. We also expose port 5432 so you can connect with a GUI like TablePlus.”
Adjust for students’ databases:
- MongoDB:
image: mongo:7, port27017, envMONGO_INITDB_DATABASE: myapp - MySQL:
image: mysql:8, port3306, envMYSQL_ROOT_PASSWORD,MYSQL_DATABASE - No database: skip the
dbservice andvolumessection
Full docker-compose.yml:
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
env_file:
- .env
volumes:
- .:/app
- /app/node_modules
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
pgdata:
Run it¶
“Watch the output. You’ll see both services start. The first run downloads the Postgres image — that’s a one-time thing.”
Check it works:
- App:
curl http://localhost:3000or open in browser - DB: use a DB GUI or
docker compose exec db psql -U user myapp
Show live reloading:
// Make a change to a route or response
// Save the file
// (If using nodemon) Watch the app restart in the compose output
// Curl the endpoint again — change is reflected
“No rebuild. The bind mount means your local file system IS the container’s file system.”
Part 6 — Key Compose Commands (5 min)¶
# Run in background
docker compose up -d
# View logs
docker compose logs -f app
# Run a command inside a running service
docker compose exec app sh # open a shell
docker compose exec app npm test # run tests inside container
# Stop everything
docker compose down
# Stop AND wipe the database volume
docker compose down -v
# Rebuild after Dockerfile changes
docker compose up --build
“Commit this to memory:
up,down,exec,logs. That’s 90% of what you’ll use day to day.”
Part 7 — Update .gitignore and Commit (2 min)¶
Should include:
git add Dockerfile .dockerignore docker-compose.yml .env.example .gitignore
git commit -m "Add Docker development environment"
git push
“Notice what we’re NOT committing:
.env,node_modules. What we ARE committing:Dockerfile,docker-compose.yml,.env.example. The setup is reproducible for any teammate.”
Part 8 — Stretch Goal: Docker in CI (10 min, if time permits)¶
“Remember the CI workflow from Lab 1? Let’s make the test job use a Postgres service — same version as our docker-compose.”
Open .github/workflows/ci.yml and update the test job:
test:
runs-on: ubuntu-latest
needs: lint
services:
db:
image: postgres:16-alpine
env:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test
env:
DATABASE_URL: postgres://user:password@localhost:5432/testdb
NODE_ENV: test
“This spins up a real Postgres container in CI — same image as your docker-compose. Your tests now run against the same database version in CI as in development. That’s CI/CD parity.”
Wrap-Up and Debrief (5 min)¶
What you built today¶
- ✅
Dockerfile— reproducible build of the Node.js app - ✅
.dockerignore— lean images, no accidentally shipped node_modules - ✅
.env.example— documented configuration for teammates - ✅
docker-compose.yml— full dev stack in one command - ✅ Bind mounts — live code reloading without rebuilds
- ✅ (Stretch) CI uses the same Postgres version as development
The combined workflow students now have¶
git clone repo
cp .env.example .env # fill in real values
docker compose up # everything running in <2 min
# Develop locally → changes reflected instantly via bind mount
# Push to GitHub → CI runs with same database version
# PR passes CI → team can merge with confidence
Discussion questions¶
- “A new teammate joins your project tomorrow. Walk them through setup using what you built today.”
- “What would break if someone forgot to add
node_modulesto.dockerignore?” - “Why do we have two entries in the
volumeslist for the app service?”
Troubleshooting Guide¶
“Port already in use”¶
# Find what's using the port
lsof -i :3000 # macOS/Linux
# or just change the left port in docker-compose.yml
ports:
- "3001:3000"
“Cannot connect to Docker daemon”¶
Docker Desktop isn’t running. Open it and wait for the whale icon to stop animating.
“node_modules not found inside container”¶
Make sure the anonymous volume line is present:
Without it, the bind mount overwrites the container’s node_modules with your (possibly empty or wrong-platform) local one.
“App can’t connect to database”¶
- In docker-compose, services communicate by service name, not
localhost. Database host should bedb(the service name), notlocalhost. - Make sure
DATABASE_URLin.envuses@db:5432, not@localhost:5432. - Check
depends_on: dbis in the app service.
“Changes to code not reflected”¶
- Confirm the bind mount is correct:
- .:/app - If using nodemon, make sure the
CMDin Dockerfile runs nodemon:CMD ["npx", "nodemon", "server.js"] - Or override in docker-compose for dev:
“docker compose down doesn’t delete my data”¶
That’s intentional — named volumes persist. Use docker compose down -v to also delete volumes (wipes the database).
“Image takes too long to build”¶
- Confirm
.dockerignoreexists and excludesnode_modulesand.git - Check
cache: 'npm'is in the setup-node step (for CI) - Move
COPY package*.json ./beforeCOPY . .
“I accidentally committed .env”¶
# Remove from tracking (keeps the local file)
git rm --cached .env
echo ".env" >> .gitignore
git commit -m "Remove .env from tracking"
git push
If the repo is public or the secrets were real, rotate the credentials immediately.
Created : July 6, 2026