Skip to main content

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.

Goal

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:

Create the project
mkdir campus-library-docker
cd campus-library-docker
Initialize and install dependencies
npm init -y
npm install express pg redis
Create server.js
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:

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"]
Build the image
docker build -t campus-library .

Step 3 · Docker Compose​

Write a compose file with web, database, and cache:

docker-compose.yml
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​

Build and start
docker compose up -d --build
Verify all services are healthy
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
Test the app
curl http://localhost:3000
{"message":"Welcome to Campus Library!","visits":1}

Run it again — the visit count increments:

Test caching
curl http://localhost:3000
{"message":"Welcome to Campus Library!","visits":2}

Step 5 · Test Persistence​

The database data survives container restarts:

Restart the database
docker compose restart db
Check the app still works
curl http://localhost:3000

The visit count might reset (Redis was restarted) but the database is intact.

Step 6 · Clean Up​

Stop everything
docker compose down
Stop and remove volumes (fresh start)
docker compose down -v
Remember

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.