Capstone — Ship Campus Library in Containers
Time for the real thing. This challenge chains everything you learned into one small project. Plan for about one hour. If you get stuck, the answer to every step is already in the pages you've read.
By the end, you should have a multi-service Campus Library app running in Docker — web server, database, and cache — with persistent data, health checks, and a production-ready Dockerfile.
Step 1 · The App
Create the Campus Library app — a simple Node.js server that connects to PostgreSQL and uses Redis for caching:
mkdir campus-library-docker
cd campus-library-docker
npm init -y
npm install express pg redis
cat > server.js << 'SERVEREOF'
const express = require('express');
const { Pool } = require('pg');
const { createClient } = require('redis');
const app = express();
const pool = new Pool({
host: process.env.DB_HOST || 'db',
port: 5432,
user: process.env.DB_USER || 'riya',
password: process.env.DB_PASSWORD || 'secret',
database: process.env.DB_NAME || 'campus_library',
});
const redis = createClient({ url: process.env.REDIS_URL || 'redis://cache:6379' });
redis.connect();
app.get('/', async (req, res) => {
try {
const cached = await redis.get('visit_count');
const count = parseInt(cached || '0') + 1;
await redis.set('visit_count', count);
res.json({ message: 'Welcome to Campus Library!', visits: count });
} catch (err) {
res.json({ message: 'Welcome to Campus Library!', visits: 'cache unavailable' });
}
});
app.get('/health', (req, res) => res.status(200).send('OK'));
app.listen(3000, () => console.log('Library running on port 3000'));
SERVEREOF
Step 2 · Dockerfile
Write a production-ready Dockerfile:
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
docker build -t campus-library .
Step 3 · Docker Compose
Write a compose file with web, database, and cache:
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DB_HOST=db
- DB_USER=riya
- DB_PASSWORD=secret
- DB_NAME=campus_library
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: campus_library
POSTGRES_USER: riya
POSTGRES_PASSWORD: secret
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U riya -d campus_library"]
interval: 5s
timeout: 3s
retries: 5
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
volumes:
db-data:
Step 4 · Start Everything
docker compose up -d --build
docker compose ps
NAME IMAGE STATUS PORTS
campus-lib-cache redis:7-alpine Up 10 seconds (healthy)
campus-lib-db postgres:16-alpine Up 10 seconds (healthy)
campus-lib-web campus-library Up 10 seconds (healthy) 0.0.0.0:3000->3000/tcp
curl http://localhost:3000
{"message":"Welcome to Campus Library!","visits":1}
Run it again — the visit count increments:
curl http://localhost:3000
{"message":"Welcome to Campus Library!","visits":2}
Step 5 · Test Persistence
The database data survives container restarts:
docker compose restart db
curl http://localhost:3000
The visit count might reset (Redis was restarted) but the database is intact.
Step 6 · Clean Up
docker compose down
docker compose down -v
docker compose down keeps volumes (data safe). docker compose down -v deletes volumes (fresh start). Know the difference.
What You Built
You chained every Docker skill:
- Dockerfile — multi-stage, non-root user, health check
- docker-compose — multi-service with dependencies and health checks
- Networking — containers communicating by name
- Volumes — persistent database data
- Environment variables — configuration without hardcoding
- Health checks — quality control for every service
This is exactly how real production applications are containerized.