π§° Weekend Project: Version Control & Docker
Time to put the whole module to work. Over a weekend, you'll scaffold a real full-stack app and wrap it in the professional plumbing that makes teams productive: a clean Git branching workflow, a reproducible Docker Compose stack, a VS Code dev container, and a GitHub Actions pipeline that runs on every push. By Sunday night you'll have a repository that any teammate can clone and run with a single command.
π― Learning Objectives
By the end of this project, you will be able to:
- Initialize a multi-service repository with a sensible structure,
.gitignore, and a feature-branch workflow - Author a
docker-compose.ymlthat runs a backend API, a frontend, and a database as networked services - Configure a VS Code dev container so the whole team develops inside the same environment
- Write a GitHub Actions CI workflow that installs, lints, and tests on every push
- Evaluate your own work against a concrete "what good looks like" checklist
Estimated Time: 4β8 hours (a weekend) β’ Difficulty: Intermediate
Hands-on: This entire lesson is the build. Work through the five milestones in order and commit as you go.
In This Lesson
The Brief & How to Work
Your goal is not to write a lot of application code β it's to build the environment and workflow around a small app so that it is reproducible, versioned, and continuously tested. The app itself can stay deliberately tiny: a backend that returns a health-check message, a frontend that displays it, and a database that proves the network wiring works.
Treat this like a professional would. Work in small, well-labelled Git commits. Do each unit of work on a branch, open a pull request, and merge it. Don't try to do everything at once β the milestones below are designed so each one leaves you with something that runs.
π What you'll produce
A repository containing a backend/, a frontend/, a docker-compose.yml, a .devcontainer/ folder, a .github/workflows/ CI file, and a real README.md.
A workflow: feature branches merged via pull requests, with CI green before merge.
A "one command up" experience: a fresh clone runs with docker compose up.
π‘ Approach it methodically. Understand the goal, plan the pieces, build one milestone at a time, then review against the checklist. That "understand β plan β execute β review" loop (Polya's classic problem-solving steps) is exactly how experienced developers tackle unfamiliar setup work.
Milestone Roadmap
Five milestones take you from an empty folder to a continuously tested, containerized app. Each depends on the one before it.
Repo & Git flow] --> M2[Milestone 2
Docker Compose] M2 --> M3[Milestone 3
Dev container] M3 --> M4[Milestone 4
CI pipeline] M4 --> M5[Milestone 5
Docs & review]
| Milestone | Deliverable | Rough time |
|---|---|---|
| 1 β Repository & Git workflow | Initialized repo, structure, .gitignore, first feature branch | 45β60 min |
| 2 β Compose the stack | Backend + frontend + database running via Compose | 90β120 min |
| 3 β Dev container | .devcontainer so VS Code reopens inside the container | 45β60 min |
| 4 β CI pipeline | GitHub Actions installs, lints, and tests on push | 60β90 min |
| 5 β Document & reflect | README, screenshots, reflection notes | 30β45 min |
Milestone 1 β Repository & Git Workflow
Start with a clean, well-structured repository and a branching habit you'll keep for the rest of the project.
Create the structure
mkdir weekend-stack && cd weekend-stack
git init
mkdir backend frontend
# a top-level README you'll fill in at Milestone 5
touch README.md docker-compose.yml
Add a real .gitignore
Never commit dependencies, build output, or secrets. A minimal but honest ignore file:
# dependencies
node_modules/
# build output
dist/
build/
# environment & secrets
.env
.env.local
# logs and OS cruft
*.log
.DS_Store
Adopt a feature-branch workflow
Keep main always working. Do each piece of work on a short-lived branch, then merge it back through a pull request.
# commit the skeleton on main
git add .
git commit -m "chore: scaffold repo structure and gitignore"
# start the first feature on its own branch
git switch -c feat/compose-stack
β οΈ Common trap
If you accidentally committed node_modules/ before adding .gitignore, Git keeps tracking it. Fix it with git rm -r --cached node_modules, then commit. The --cached flag removes it from Git without deleting it from disk.
Milestone 2 β Compose the Stack
Now make three services run together with one command. We'll use a Node/Express backend, a static frontend, and a Postgres database β but the shape is identical if you swap in Python or PHP.
A tiny backend
backend/server.js β a single health-check endpoint. Modern syntax: const, arrow functions, and a route that reports the database host it was told to use.
const express = require('express');
const app = express();
const PORT = process.env.PORT || 4000;
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
db: process.env.DB_HOST || 'not configured',
time: new Date().toISOString(),
});
});
app.listen(PORT, () => {
console.log(`API listening on http://localhost:${PORT}`);
});
backend/package.json β declare the start scripts so the container knows how to run and test the app.
{
"name": "backend",
"version": "1.0.0",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js",
"test": "node --test"
},
"dependencies": {
"express": "^4.19.2"
}
}
Dockerfile for the backend
backend/Dockerfile β copy manifests first so Docker can cache the dependency layer.
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 4000
CMD ["npm", "start"]
Wire it together with Compose
docker-compose.yml β three services on one network. Note the modern Compose file needs no version: key. The backend reaches the database using the service name db as its hostname.
services:
backend:
build: ./backend
ports:
- "4000:4000"
environment:
DB_HOST: db
DB_USER: appuser
DB_PASSWORD: apppass
DB_NAME: appdb
depends_on:
- db
frontend:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./frontend:/usr/share/nginx/html:ro
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: apppass
POSTGRES_DB: appdb
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
A frontend that proves the wiring
frontend/index.html β fetches the backend health check and shows it. This confirms the browser can reach the API.
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Weekend Stack</title></head>
<body>
<h1>Weekend Stack</h1>
<pre id="out">loadingβ¦</pre>
<script>
fetch('http://localhost:4000/api/health')
.then((r) => r.json())
.then((data) => {
document.getElementById('out').textContent = JSON.stringify(data, null, 2);
})
.catch((err) => {
document.getElementById('out').textContent = 'API unreachable: ' + err;
});
</script>
</body>
</html>
Bring it up
docker compose up --build
Visit http://localhost:8080 and you should see:
{
"status": "ok",
"db": "db",
"time": "2026-07-30T12:00:00.000Z"
}
db), while your machine reaches them through the published ports.Milestone 3 β Dev Container
A dev container guarantees every teammate develops with the same Node version, extensions, and settings β no more "works on my machine." VS Code reads a .devcontainer/devcontainer.json and reopens your project inside a container.
{
"name": "Weekend Stack",
"dockerComposeFile": "../docker-compose.yml",
"service": "backend",
"workspaceFolder": "/app",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
],
"settings": {
"editor.formatOnSave": true
}
}
},
"forwardPorts": [4000, 8080],
"postCreateCommand": "npm install"
}
Open the folder in VS Code, then run Dev Containers: Reopen in Container from the command palette. VS Code builds (or reuses) the backend service, installs extensions inside it, and opens a terminal that lives in the container.
π‘ Why point the dev container at a Compose service?
By reusing the same docker-compose.yml, your editor's container and your running stack are the same environment. When you start a debug session, the backend can already reach db by name because it's on the Compose network.
Milestone 4 β CI with GitHub Actions
Continuous Integration runs your checks automatically on every push, so a broken commit is caught before it reaches main. Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: ["**"]
pull_request:
branches: ["main"]
jobs:
backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: backend/package-lock.json
- run: npm ci
- run: npm test
Add one small test so npm test actually verifies something. backend/health.test.js using Node's built-in test runner (no extra dependency):
const { test } = require('node:test');
const assert = require('node:assert');
test('health payload is well formed', () => {
const payload = { status: 'ok', db: 'db' };
assert.strictEqual(payload.status, 'ok');
assert.ok(payload.db, 'db host should be set');
});
Commit, push your feature branch, and open a pull request. GitHub runs the workflow and shows a green check when it passes.
install Β· test} T -->|pass β | M[Safe to merge PR] T -->|fail β| F[Fix & push again] F --> T
β οΈ Use npm ci, not npm install, in CI
npm ci installs exactly what the lockfile pins β deterministic and fast β and fails loudly if package.json and package-lock.json disagree. That's exactly what you want a pipeline to catch.
Milestone 5 β Document & Reflect
A project isn't done until someone else can run it. Write a README.md that a newcomer could follow cold.
# Weekend Stack
A minimal full-stack app used to practice Git, Docker Compose,
dev containers, and CI.
## Stack
- Backend: Node.js + Express
- Frontend: static HTML served by nginx
- Database: PostgreSQL
## Run it
```bash
docker compose up --build
```
- Frontend: http://localhost:8080
- API health: http://localhost:4000/api/health
## Develop
Open in VS Code and choose "Reopen in Container".
## Workflow
Feature branches β pull request β CI green β merge to `main`.
Reflect (write 4β6 sentences in the README)
- What was the hardest part to get working, and how did you diagnose it?
- Which milestone gave the biggest payoff for the effort?
- One thing you'd add next: automated end-to-end tests? A production Dockerfile? A staging deploy?
π‘ Stretch goals (optional)
- Swap the backend for Flask or Laravel β same Compose shape, different
Dockerfile. - Add a lint step (ESLint/Prettier) to the CI job.
- Add a healthcheck to the
dbservice and makebackendwait for it. - Cache Docker layers in CI to speed up builds.
What Good Looks Like
Before you call it finished, compare your work against these standards. This is roughly how a reviewer or instructor would grade it.
| Area | β Needs work | β What good looks like |
|---|---|---|
| Git history | One giant "final" commit; secrets or node_modules committed |
Small, purposeful commits with clear messages; work merged via PRs; nothing ignored is tracked |
| Compose | Services can't reach each other; hard-coded IPs | Services addressed by name; one docker compose up starts everything |
| Dev container | Absent, or doesn't open | Reopen-in-container works and installs the pinned toolchain + extensions |
| CI | No workflow, or it's permanently red | Green on push; uses npm ci; a real (if small) test runs |
| Docs | Empty or default README | Clear run steps a newcomer can follow, plus a short reflection |
β The one-sentence test
If a teammate can clone your repo, run docker compose up, and see the health check in the browser without asking you a single question, you've built it right.
Completion Checklist
Tick these off before you consider the weekend project done:
- β Repo initialized with
backend/,frontend/, and a working.gitignore - β At least two feature branches merged into
mainvia pull requests - β
docker compose up --buildstarts backend, frontend, and database - β The frontend page displays the backend's health JSON
- β Backend reaches the database by its service name (
db) - β
.devcontainer/devcontainer.jsonreopens the project in a container - β
.github/workflows/ci.ymlruns and passes on push - β CI uses
npm ciand runs at least one test - β
README.mdhas run instructions + a short reflection - β No secrets,
node_modules, or build output tracked in Git
π― Quick Quiz
Question 1: In a Docker Compose stack, how does the backend service reach the db service?
Question 2: Why does the CI workflow use npm ci instead of npm install?
Question 3: You committed node_modules/ by mistake before adding .gitignore. What removes it from version control while keeping it on disk?
Summary & Quiz
π Key Takeaways
- The value of this project is the environment and workflow, not a lot of app code.
- Git feature branches + PRs keep
mainworking and make review easy. - Docker Compose runs multiple services on one network; they address each other by service name.
- Dev containers give everyone the same toolchain; CI catches breakage on every push.
- "Done" means a teammate can clone and run it with one command.
π Further Reading
- Docker Compose documentation
- VS Code Dev Containers
- GitHub Actions documentation
- Feature-branch workflow (Atlassian)
π What's Next?
You've closed out Module 2 with a reproducible, tested project. Next, in Module 3, we zoom back out to the web itself β how browsers, servers, HTTP, and the wider technology ecosystem fit together β so your future features rest on a solid mental model.
π Weekend well spent!
You now have a repo that runs anywhere with one command. That habit alone will save you countless hours.