Back to all posts

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.

Docker

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

  1. User opens the app on port 8080.
  2. Nginx serves frontend and proxies API calls to backend.
  3. Frontend calls GET /api/products.
  4. Backend checks Redis cache:
    • Cache hit: return products immediately.
    • Cache miss: read products from PostgreSQL, then cache them.
  5. Frontend calls POST /api/order when user clicks Buy.
  6. Backend stores order in PostgreSQL with status pending.
  7. Backend publishes order event to RabbitMQ.
  8. Worker consumes order message, waits briefly, then updates order status to completed.

Architecture Diagram

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

  1. Build and run all services.
docker compose up --build
  1. Open app: http://localhost:8080
  2. 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:

  1. Ensure compose uses exact values:
    • POSTGRES_USER=admin
    • POSTGRES_PASSWORD=admin
    • POSTGRES_DB=appdb
  2. 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 used products_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:

  • pg client returns result.rows, not result.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 Modified is 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.sql runs 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