10. # Docker ComposeΒΆ
- its multi container concept
- Docker Compose is a tool you can use to define and share multi-container applications. This means you can run a project with multiple containers using a single source.
- docker-compose.yml is a YML file defining services, networks, and volumes for a Docker container
- tool for defining and running multi-container applications. These tools integrate tightly with code repositories (such as GitHub) and continuous integration and continuous delivery (CI/CD) pipeline tools (such as Jenkins).
- docker compose build - to build our multi-container using docker build
- docker-compose build --no-cache ==> downloads images everytime no local reuse
- docker compose up - to run the containers [this will not look for change in docker file if images are already present hence we use below command ]
- docker-compose up --build => Rebuilds images with latest changes in code
-
docker-compose up -d ==> ups in detached mode
-
docker compose ps - to check container list.
- docker compose ps -a -
- docker-compose logs - to see logs of running services
- docker-compose logs
=> to see logs of specific service
- docker-compose logs
- docker logs
=> view logs - docker logs -f
=> Follow logs - docker-compose ps => list containers
- docker-compose up --scale worker=5 => Scaling Services
- docker-compose exec app sh => Executing Commands Inside Services
- curl localhost - > gives error of curl not found if curl is not installed
- exit and edit DockerFIle , to add curl with APK
- RUN api add --update curl
- docker-compose up -d but it will still give you same error because it didn't rebuild image
- docker-compse up -d --build ==> this will rebuild image
- now if you try docker-compose exec web sh ==> curl localhost ==> it will give error
docker-compose.override.yml= > Overrides are great for dev-mode mounts.- docker-compose exec service sh
- docker-compose config
- docker-compsoe down --help ==> gives you options to delete images and other things
-
docker-compose down -v => Cleaning Up Compose Resources
-
docker-compose stop - to stop all services
- docker-compose down --volumes -> o bring everything down, removing the containers entirely, with the data volume of the services:
5. Services SectionΒΆ
Each service defines a container.
Example:
services:
api:
image: node:18
command: ["npm", "start"]
12. HealthchecksΒΆ
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000"]
interval: 10s
retries: 5
13. Restart PoliciesΒΆ
restart: always
Below is a complete ready-to-copy Markdown article containing:
β Explanation
β Full server.js
β Multi-stage Dockerfile
β docker-compose.dev.yml
β docker-compose.prod.yml
β Directory structure
β How to run Dev & Prod
You can paste this directly in your blog.
π³ Building a Production-Ready Multi-Container Express App (Redis + Postgres + Docker)ΒΆ
In this guide, we will create a fully production-ready Node.js Express app using:
-
Redis β for caching & fast counter increments
-
Postgres β for persistent storage
-
Docker Compose β multi-container orchestration
-
Multi-stage Dockerfile β optimized dev & production images
-
Healthchecks β ensuring app starts only when DB + Redis are ready
-
Graceful shutdown
-
Retry logic with exponential backoff
-
Automatic database migrations
You can copy the entire setup below and run it as-is.
π Project StructureΒΆ
/your-app
βββ server.js
βββ package.json
βββ Dockerfile
βββ docker-compose.dev.yml
βββ docker-compose.prod.yml
π 1. Express Server with Redis + PostgresΒΆ
This server.js includes:
β Redis connection with retry + backoff
β Postgres connection with retry + backoff
β Graceful shutdown
β Auto migrations
β Increment API endpoint
server.jsΒΆ
const express = require("express");
const { Client } = require("pg");
const Redis = require("ioredis");
const PORT = process.env.PORT || 3000;
const POSTGRES_URL = process.env.POSTGRES_URL;
const REDIS_URL = process.env.REDIS_URL;
const app = express();
app.use(express.json());
// ------------------------------
// Retry Helper (Exponential Backoff)
// ------------------------------
async function retry(fn, retries = 10, delay = 1000) {
try {
return await fn();
} catch (err) {
if (retries === 0) throw err;
console.log(`Retry failed. Retrying in ${delay}ms...`);
await new Promise(res => setTimeout(res, delay));
return retry(fn, retries - 1, delay * 2);
}
}
// ------------------------------
// Postgres Connection
// ------------------------------
const pg = new Client({
connectionString: POSTGRES_URL,
});
async function connectPostgres() {
await retry(async () => {
console.log("Connecting to Postgres...");
await pg.connect();
console.log("Connected to Postgres!");
});
}
// ------------------------------
// Redis Connection
// ------------------------------
let redis;
async function connectRedis() {
await retry(async () => {
console.log("Connecting to Redis...");
redis = new Redis(REDIS_URL);
await redis.ping();
console.log("Connected to Redis!");
});
}
// ------------------------------
// Migrations
// ------------------------------
async function runMigrations() {
console.log("Running migrations...");
await pg.query(`
CREATE TABLE IF NOT EXISTS counters (
id VARCHAR(50) PRIMARY KEY,
value INTEGER NOT NULL
);
`);
console.log("Migrations complete.");
}
// ------------------------------
// API Route
// ------------------------------
app.get("/increment", async (req, res) => {
const key = "counter1";
let newValue = await redis.incr(key);
await pg.query(
`
INSERT INTO counters (id, value)
VALUES ($1, $2)
ON CONFLICT (id) DO UPDATE SET value = $2
`,
[key, newValue]
);
res.json({ counter: newValue });
});
// ------------------------------
// Graceful Shutdown
// ------------------------------
function gracefulShutdown() {
console.log("Shutting down gracefully...");
redis && redis.quit();
pg && pg.end();
server.close(() => {
console.log("HTTP server closed.");
process.exit(0);
});
}
process.on("SIGINT", gracefulShutdown);
process.on("SIGTERM", gracefulShutdown);
// ------------------------------
// Startup Sequence
// ------------------------------
async function start() {
await connectRedis();
await connectPostgres();
await runMigrations();
global.server = app.listen(PORT, () =>
console.log(`Server running on ${PORT}`)
);
}
start().catch(err => {
console.error("Startup failed:", err);
process.exit(1);
});
π οΈ 2. Multi-Stage Dockerfile (Dev + Prod)ΒΆ
This Dockerfile builds:
-
Dev image (with live reload)
-
Production image (optimized)
DockerfileΒΆ
# -------------------------
# BASE IMAGE (Shared)
# -------------------------
FROM node:18-alpine AS base
WORKDIR /app
ENV NODE_ENV=production
# -------------------------
# DEVELOPMENT STAGE
# -------------------------
FROM base AS development
ENV NODE_ENV=development
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
# -------------------------
# PRODUCTION BUILD
# -------------------------
FROM base AS build
COPY package*.json ./
RUN npm install --only=production
COPY . .
# -------------------------
# PRODUCTION RUNTIME
# -------------------------
FROM node:18-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app /app
EXPOSE 3000
CMD ["node", "server.js"]
π³ 3. Docker Compose (Development)ΒΆ
This is meant for local development:
-
Hot reloading
-
Mounted volumes
-
Healthchecks
-
Node runs via nodemon
docker-compose.dev.ymlΒΆ
version: "3.8"
services:
app:
build:
context: .
target: development
container_name: app_dev
ports:
- "3000:3000"
environment:
- REDIS_URL=redis://redis:6379
- POSTGRES_URL=postgres://postgres:postgres@postgres:5432/appdb
depends_on:
redis:
condition: service_healthy
postgres:
condition: service_healthy
volumes:
- .:/app
- /app/node_modules
command: ["npm", "run", "dev"]
redis:
image: redis:7
container_name: redis_dev
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
postgres:
image: postgres:15
container_name: pg_dev
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: appdb
ports:
- "5432:5432"
volumes:
- pg-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 10
volumes:
pg-data:
π 4. Docker Compose (Production)ΒΆ
Production:
-
No volumes
-
Lean build
-
Healthchecks
-
Clean image
docker-compose.prod.ymlΒΆ
version: "3.8"
services:
app:
build:
context: .
target: production
container_name: app_prod
ports:
- "3000:3000"
environment:
- REDIS_URL=redis://redis:6379
- POSTGRES_URL=postgres://postgres:postgres@postgres:5432/appdb
depends_on:
redis:
condition: service_healthy
postgres:
condition: service_healthy
redis:
image: redis:7
container_name: redis_prod
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
postgres:
image: postgres:15
container_name: pg_prod
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: appdb
volumes:
- pg-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 10
volumes:
pg-data:
βΆοΈ Running the AppΒΆ
Development with Hot ReloadingΒΆ
docker compose -f docker-compose.dev.yml up --build
Hit the API:
GET http://localhost:3000/increment
Production ModeΒΆ
docker compose -f docker-compose.prod.yml up --build -d
π Final NotesΒΆ
This setup gives you:
-
Containerized Express API
-
Redis caching layer
-
Postgres persistence
-
Health-checked startup
-
Automatic migrations
-
Hot reload for development
-
Minimal production images
-
Graceful shutdown safety
You can deploy this in:
β Docker Swarm
β Kubernetes
β AWS ECS
β Any Linux server