How E-commerce Websites Actually Work: Full System Simulation with Docker
•5 min read
DockerE-commerceNext.jsMicroservicesPostgreSQLRedisRabbitMQ
Why I Built This
Most tutorials show only frontend + one backend API. Real e-commerce systems are more than that.
I wanted to simulate a realistic architecture where:
- API responses are cached for speed.
- Orders are processed asynchronously.
- Multiple services communicate over a private Docker network.
This project is a practical mini version of production style e-commerce request flow.

Tech Stack
- Frontend: HTML + CSS (served through Nginx)
- Backend: Node.js + Express
- Database: PostgreSQL
- Cache: Redis
- Queue: RabbitMQ
- Worker: Node.js consumer
- Orchestration: Docker Compose
Folder Structure
ecommerce-docker/
|- docker-compose.yml
|- init.sql
|- backend/
| |- Dockerfile
| |- package.json
| `- server.js
|- frontend/
| |- Dockerfile
| `- index.html
|- nginx/
| `- default.conf
`- worker/
|- Dockerfile
`- worker.js
Code Walkthrough
docker-compose.yml
version: "3"
services:
frontend:
build: ./frontend
backend:
build: ./backend
depends_on:
- postgres
- redis
- rabbitmq
worker:
build: ./worker
depends_on:
- rabbitmq
- postgres
postgres:
image: postgres:15
environment:
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=admin
- POSTGRES_DB=appdb
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
redis:
image: redis:7
rabbitmq:
image: rabbitmq:3-management
ports:
- "15672:15672"
nginx:
image: nginx
ports:
- "8080:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- frontend
- backend
nginx/default.conf
server {
listen 80;
location / {
proxy_pass http://frontend;
}
location /api/ {
rewrite ^/api/(.*) /$1 break;
proxy_pass http://backend:5001;
}
}
init.sql
CREATE TABLE products(
id SERIAL PRIMARY KEY,
name TEXT,
price INT
);
CREATE TABLE orders(
id SERIAL PRIMARY KEY,
product_id INT,
status TEXT
);
INSERT INTO products(name, price) VALUES ('Laptop', 50000), ('Phone', 20000);
backend/server.js
const express = require("express");
const { Pool } = require("pg");
const redis = require("redis");
const amqp = require("amqplib");
const app = express();
app.use(express.json());
const pool = new Pool({
host: "postgres",
user: "admin",
password: "admin",
database: "appdb",
port: 5432,
});
const redisClient = redis.createClient({ url: "redis://redis:6379" });
redisClient.connect();
let channel;
async function connectRabbitMQ() {
while (true) {
try {
const conn = await amqp.connect("amqp://rabbitmq");
channel = await conn.createChannel();
await channel.assertQueue("orders");
console.log("Connected to RabbitMQ");
break;
} catch (err) {
console.log("Waiting for RabbitMQ...");
await new Promise((res) => setTimeout(res, 3000));
}
}
}
connectRabbitMQ();
app.get("/products", async (req, res) => {
try {
const cached = await redisClient.get("products");
if (cached) {
return res.json(JSON.parse(cached));
}
const result = await pool.query("SELECT * FROM products");
await redisClient.set("products", JSON.stringify(result.rows));
return res.json(result.rows);
} catch (err) {
console.error("Failed to load products:", err.message);
return res.status(500).json({ error: "Failed to load products" });
}
});
app.post("/order", async (req, res) => {
try {
const { product_id } = req.body;
const result = await pool.query(
"INSERT INTO orders(product_id, status) VALUES($1, $2) RETURNING *",
[product_id, "pending"],
);
channel.sendToQueue("orders", Buffer.from(JSON.stringify(result.rows[0])));
return res.json({
message: "Order Placed Successfully",
order: result.rows[0],
});
} catch (err) {
console.error("Failed to place order:", err.message);
return res.status(500).json({ error: "Failed to place order" });
}
});
app.listen(5001, () => {
console.log("Backend running on 5001");
});
worker/worker.js
const amqp = require("amqplib");
const { Pool } = require("pg");
const pool = new Pool({
host: "postgres",
user: "admin",
password: "admin",
database: "appdb",
port: 5432,
});
async function startWorker() {
let channel;
while (true) {
try {
const conn = await amqp.connect("amqp://rabbitmq");
channel = await conn.createChannel();
await channel.assertQueue("orders");
console.log("Worker connected to RabbitMQ");
break;
} catch (err) {
console.log("Waiting for RabbitMQ...");
await new Promise((res) => setTimeout(res, 3000));
}
}
console.log("Worker started...");
channel.consume("orders", async (msg) => {
const order = JSON.parse(msg.content.toString());
console.log("Processing order:", order.id);
setTimeout(async () => {
await pool.query("UPDATE orders SET status='completed' WHERE id=$1", [
order.id,
]);
console.log("Order completed:", order.id);
channel.ack(msg);
}, 5000);
});
}
startWorker();
frontend/index.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Products</title>
<style>
:root {
--bg: #f5f9ff;
--text: #1f2a44;
--primary: #2a7fff;
--primary-hover: #1566de;
--card: #ffffff;
--card-border: #d9e6ff;
}
body {
font-family: Arial, sans-serif;
background: var(--bg);
color: var(--text);
margin: 0;
padding: 28px;
}
h1 {
margin-top: 0;
color: #173a75;
}
button {
background: var(--primary);
color: #fff;
border: none;
border-radius: 6px;
padding: 10px 14px;
cursor: pointer;
}
button:hover {
background: var(--primary-hover);
}
#list {
list-style: none;
padding: 0;
margin-top: 18px;
max-width: 520px;
}
#list li {
background: var(--card);
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
#list li button {
padding: 8px 12px;
}
</style>
</head>
<body>
<h1>Products</h1>
<button onclick="load()">Load Products</button>
<ul id="list"></ul>
<script>
async function buy(id) {
await fetch("/api/order", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ product_id: id }),
});
alert("Order Placed");
}
async function load() {
const res = await fetch("/api/products");
const data = await res.json();
const list = document.getElementById("list");
list.innerHTML = "";
data.forEach((e) => {
const li = document.createElement("li");
li.innerHTML = `<span>${e.name} - Rs. ${e.price}</span>
<button onclick="buy(${e.id})">Buy</button>`;
list.appendChild(li);
});
}
</script>
</body>
</html>
Architecture At A Glance
- User opens the app on port 8080.
- Nginx serves frontend and proxies API calls to backend.
- Frontend calls GET /api/products.
- Backend checks Redis cache:
- Cache hit: return products immediately.
- Cache miss: read products from PostgreSQL, then cache them.
- Frontend calls POST /api/order when user clicks Buy.
- Backend stores order in PostgreSQL with status pending.
- Backend publishes order event to RabbitMQ.
- Worker consumes order message, waits briefly, then updates order status to completed.

Services And Ports
- Nginx: http://localhost:8080
- RabbitMQ dashboard: http://localhost:15672
- RabbitMQ default login: guest / guest
- Internal service connections inside Docker network:
- backend -> postgres:5432
- backend -> redis:6379
- backend and worker -> rabbitmq:5672
API Endpoints
GET /api/products
Returns product list from Redis cache or PostgreSQL.
POST /api/order
Creates a pending order and enqueues async processing.
Request body:
{
"product_id": 1
}
Run Locally
Prerequisites
- Docker Desktop (or Docker Engine + Compose)
Start project
- Build and run all services.
docker compose up --build
- Open app: http://localhost:8080
- Click Load Products, then Buy.
Run in detached mode
docker compose up --build -d
Stop project
docker compose down
Stop and remove volumes
Use this when database init or credentials changed:
docker compose down -v
Common Errors I Faced (And Fixes)
1) PostgreSQL authentication failed for user admin (28P01)
Typical log:
error: password authentication failed for user "admin"
Why this happens:
- Environment values in compose have accidental spaces after
=. - Example bad value:
POSTGRES_USER= admin
Fix:
- Ensure compose uses exact values:
POSTGRES_USER=adminPOSTGRES_PASSWORD=adminPOSTGRES_DB=appdb
- Recreate containers with volumes so postgres initializes with corrected env:
docker compose down -v
docker compose up --build
Note: PostgreSQL initialization env values apply when volume is first created.
2) Column products_id does not exist in orders (42703)
Typical log:
error: column "products_id" of relation "orders" does not exist
Why this happens:
- Schema column is
product_id, but SQL insert usedproducts_id.
Fix:
INSERT INTO orders(product_id, status) VALUES($1, $2) RETURNING *
3) Backend crashes and Nginx returns 502 on POST /api/order
Typical logs:
upstream prematurely closed connection while reading response header from upstream
POST /api/order ... 502
backend exited with code 1
Why this happens:
- Unhandled exception in backend route causes Node process to exit.
Fix:
- Wrap route logic in try/catch and return JSON error response.
4) result.row is undefined
Why this happens:
pgclient returnsresult.rows, notresult.row.
Fix:
- Use
result.rows[0].
5) Clicking Load Products shows Order Placed alert
Why this happens:
- Buy function or alert block is accidentally nested inside load function.
Fix:
- Keep load function only for product fetch/render.
- Keep order alert only inside buy function after successful POST /api/order.
6) GET /api/products returns 304 and looks suspicious
Why this happens:
304 Not Modifiedis browser cache validation, not backend failure.
Fix:
- Usually no action needed.
- Hard refresh if you expect fresh assets or data behavior.
7) RabbitMQ reconnect or wait messages
Typical log:
Waiting for RabbitMQ...
Why this happens:
- Startup race condition: app starts before RabbitMQ is fully ready.
Fix:
- Keep retry loop in backend and worker connection logic.
- Optionally add healthchecks and wait strategy.
8) Fresh schema/data not reflected after changing init.sql
Why this happens:
init.sqlruns only on first DB initialization.
Fix:
docker compose down -v
docker compose up --build
Useful Commands For Debugging
docker compose ps
docker compose logs backend
docker compose logs postgres
docker compose logs worker
docker compose logs nginx
docker compose logs rabbitmq
Follow logs live:
docker compose logs -f backend worker postgres nginx rabbitmq
Rebuild only one service:
docker compose up -d --build backend