Docker Compose — REST API + Postgres + Redis
Build a Node.js REST API backed by PostgreSQL for persistence and Redis for caching — a modern three-tier application stack.
Architecture
┌───────────────────────────────────────────────────┐
│ Docker Network: api-net │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Node.js │ │ Postgres │ │ Redis │ │
│ │ API │→ │ :5432 │ │ :6379 │ │
│ │ :3000 │→ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
│ │ │ │ │
│ ./app/ pg-data redis-data │
│ (build ctx) (named vol) (named vol) │
└───────────────────────────────────────────────────┘
Project Setup
mkdir api-compose && cd api-compose
mkdir app
Step 1 — Create the API Application
Create app/package.json:
{
"name": "compose-api",
"version": "1.0.0",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"express": "^4.18.0",
"pg": "^8.11.0",
"redis": "^4.6.0"
}
}
Create app/server.js:
const express = require('express');
const { Pool } = require('pg');
const { createClient } = require('redis');
const app = express();
app.use(express.json());
// PostgreSQL connection
const pool = new Pool({
host: process.env.PGHOST || 'postgres',
port: process.env.PGPORT || 5432,
database: process.env.PGDATABASE || 'apidb',
user: process.env.PGUSER || 'apiuser',
password: process.env.PGPASSWORD || 'apipass',
});
// Redis connection
const redis = createClient({ url: 'redis://redis:6379' });
redis.connect().catch(console.error);
// Health endpoint
app.get('/health', async (req, res) => {
const pgOk = await pool.query('SELECT 1').then(() => true).catch(() => false);
const redisOk = redis.isReady;
res.json({ status: pgOk && redisOk ? 'healthy' : 'degraded', pg: pgOk, redis: redisOk });
});
// Items CRUD with Redis caching
app.get('/items', async (req, res) => {
const cached = await redis.get('items').catch(() => null);
if (cached) return res.json({ source: 'cache', data: JSON.parse(cached) });
const { rows } = await pool.query('SELECT * FROM items ORDER BY id');
await redis.setEx('items', 60, JSON.stringify(rows));
res.json({ source: 'database', data: rows });
});
app.post('/items', async (req, res) => {
const { name } = req.body;
const { rows } = await pool.query('INSERT INTO items(name) VALUES($1) RETURNING *', [name]);
await redis.del('items'); // invalidate cache
res.status(201).json(rows[0]);
});
app.listen(3000, () => console.log('API running on port 3000'));
Create app/init.sql:
CREATE TABLE IF NOT EXISTS items (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
Step 2 — Create the Dockerfile
Create app/Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Step 3 — The Compose File
Create compose.yml:
services:
api:
build:
context: ./app
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "3000:3000"
environment:
PGHOST: postgres
PGDATABASE: apidb
PGUSER: apiuser
PGPASSWORD: apipass
networks:
- api-net
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: apidb
POSTGRES_USER: apiuser
POSTGRES_PASSWORD: apipass
volumes:
- pg-data:/var/lib/postgresql/data
- ./app/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- api-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U apiuser -d apidb"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis-data:/data
networks:
- api-net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pg-data:
redis-data:
networks:
api-net:
driver: bridge
Step 4 — Build and Run
docker compose up -d --build
Expected output:
[+] Building 15.2s (10/10) FINISHED
[+] Running 4/4
✔ Network api-compose_api-net Created
✔ Volume "api-compose_pg-data" Created
✔ Volume "api-compose_redis-data" Created
✔ Container api-compose-postgres-1 Healthy
✔ Container api-compose-redis-1 Healthy
✔ Container api-compose-api-1 Started
Step 5 — Test the API
Check health:
curl http://localhost:3000/health
Expected output:
{"status":"healthy","pg":true,"redis":true}
Create an item:
curl -X POST http://localhost:3000/items \
-H "Content-Type: application/json" \
-d '{"name": "first item"}'
Expected output:
{"id":1,"name":"first item","created_at":"2024-01-15T12:00:00.000Z"}
Fetch items (first call — from database):
curl http://localhost:3000/items
{"source":"database","data":[{"id":1,"name":"first item","created_at":"2024-01-15T12:00:00.000Z"}]}
Fetch again (second call — from Redis cache):
curl http://localhost:3000/items
{"source":"cache","data":[{"id":1,"name":"first item","created_at":"2024-01-15T12:00:00.000Z"}]}
Step 6 — View Logs
docker compose logs api --tail 10
api-1 | API running on port 3000
Enhancements to Try
- Add Compose Watch for hot-reload during development
- Add Adminer as a database admin UI on a
debugprofile - Add rate limiting with Redis
- Add a frontend service (React/Vue) that consumes the API
Quick Reference
| Item | Detail |
|---|---|
| API port | 3000:3000 |
| Postgres image | postgres:16-alpine |
| Redis image | redis:7-alpine |
| PG health check | pg_isready -U apiuser -d apidb |
| Redis health check | redis-cli ping |
| Build context | ./app |