Level: Intermediate
Demo application: Globomantics — e-commerce portal refactored into microservices
Table of Contents
- Course Overview
- Dockerizing a Basic React Application
- Multi-container Application with Docker Compose
- Improving the Multi-container Application
- Debugging and Production
- Reference Tables
- Practical Scenarios
1. Course Overview
This course teaches how to Dockerize a React application and make it production-ready. The business scenario is Globomantics, a company whose monolithic e-commerce portal is being refactored into microservices to resolve performance issues.
Learning Objectives
- Understand
Dockerfilecomponents and build a custom Docker image - Deploy multiple interconnected containers with Docker Compose
- Use official Docker images: Nginx, Redis, Postgres
- Overcome common issues in containerized development
- Debug a running container application from VS Code
- Prepare a React application for production with a multi-stage build
2. Dockerizing a Basic React Application
2.1 Creating the React Application
npx create-react-app myapp
cd myapp
npm start
2.2 Dockerfile Components
Each Dockerfile instruction creates a layer in the image.
# Layer 1 — Base image
FROM node:alpine
# Layer 2 — Working directory in container
WORKDIR /app
# Layer 3 — Copy package.json FIRST (cache optimization)
COPY package.json .
# Layer 4 — Install dependencies in container
RUN npm install
# Layer 5 — Copy all source code
COPY . .
# Layer 6 — Start command
CMD ["npm", "start"]
Why copy
package.jsonseparately?
Docker caches each layer. Ifpackage.jsonhasn’t changed, Docker reuses thenpm installcache and doesn’t re-execute it. This significantly reduces build times for code changes.
Build and run commands:
docker build -t myapp .
docker image ls
docker run myapp
2.3 Build Optimization with .dockerignore
Without .dockerignore, the build context would include node_modules (~181 MB).
# .dockerignore
**/node_modules
Result: Build context drops from 181 MB to approximately 583 KB.
2.4 Port Mapping — External Access
By default, a container is network-isolated. Port mapping bridges the host and container.
# Syntax: -p <host_port>:<container_port>
docker run -p 5000:3000 myapp
Access http://localhost:5000 in your browser.
Architecture: React in Docker Development
graph TD
DEV[Developer] -->|Modifies source code| SRC[Source Code\nHost Machine]
SRC -->|Volume Mount| CONT[Docker Container\nReact Dev Server :3000]
CONT -->|Hot Reload| BROWSER[Browser\nlocalhost:5000]
subgraph "Port Mapping"
HOST_PORT[Host Port :5000] -->|-p 5000:3000| CONT_PORT[Container Port :3000]
end
3. Multi-container Application with Docker Compose
3.1 Multi-container Architecture
| Component | Role | Docker Image |
|---|---|---|
| React UI | User interface | Node:alpine (custom) |
| Express API | REST API server | Node:alpine (custom) |
| Redis | Key-value database (cache, counters) | redis (official) |
graph LR
USER[User] -->|HTTP :3000| UI[React UI\nContainer :3000]
UI -->|HTTP :8080| API[Express API\nContainer :8080]
API -->|TCP :6379| REDIS[Redis\nContainer :6379]
subgraph "Docker Compose Network"
UI
API
REDIS
end
3.2 Networking Between Containers
# docker-compose.yml — Initial version (Express + Redis)
version: '3'
services:
redis-server:
image: redis
# "redis-server" becomes the DNS hostname in Docker Compose network
node-app:
build:
context: .
ports:
- "8080:8080"
In Express server index.js, use the service name as hostname:
const redis = require('redis');
const client = redis.createClient({
host: 'redis-server', // Matches service name in docker-compose.yml
port: 6379
});
Docker Compose networking magic: All services in the same
docker-compose.ymlautomatically join a shared bridge network. They can resolve each other by their service name.
3.3 Adding the React Component to Docker Compose
Directory structure:
project-root/
├── docker-compose.yml
├── frontend/ # React application
│ ├── Dockerfile
│ └── src/
└── server/ # Express server
├── Dockerfile
└── index.js
version: '3'
services:
redis-server:
image: redis
node-app:
build:
context: ./server
container_name: api-server
ports:
- "8080:8080"
ui:
build:
context: ./frontend
ports:
- "3000:3000"
stdin_open: true # Interactive mode for react-scripts start
In React’s package.json, configure the proxy to redirect API calls:
{
"proxy": "http://api-server:8080"
}
3.4 Hot Reloading with Docker Volumes
Docker images are read-only layers. For code changes to reflect without rebuilding, use a volume mount.
Via Docker Compose:
services:
ui:
build:
context: ./frontend
ports:
- "3000:3000"
stdin_open: true
volumes:
- ./frontend:/app # Source code mount
- /app/node_modules # Preserves container's node_modules
The second volume
/app/node_modulesis crucial: it tells Docker not to overwrite the container’snode_moduleswith the host’s (which may not exist).
3.5 Essential Docker Compose Commands
| Command | Description |
|---|---|
docker-compose up | Start all services (foreground) |
docker-compose up -d | Start in detached mode (background) |
docker-compose up --build | Rebuild + start |
docker-compose down | Stop and remove containers + networks |
docker-compose stop | Stop containers (without removal) |
docker-compose logs <service> | View service logs |
docker-compose ps | Status of all containers + ports |
docker-compose images | List Docker Compose images + sizes |
stopvsdown:stophalts processes but keeps images and volumes.downalso removes containers and networks.
3.6 Recovering Disk Space
# Images
docker image ls
docker image prune # Remove dangling images (untagged)
# Containers
docker container ps -a
docker container prune # Remove all stopped containers
# Volumes
docker volume ls
docker volume prune # Remove dangling volumes
# Complete Docker Compose cleanup
docker-compose down
docker volume prune
4. Improving the Multi-container Application
4.1 Extended Architecture with Postgres
| Component | Role | Port |
|---|---|---|
| React UI | Interface (Add/Check/Sell) | 3000 |
| Express API | REST server | 8080 |
| Redis | Cache — inventory counter | 6379 |
| Postgres | Relational DB — inventory totals | 5432 |
| Nginx | Proxy server — traffic routing | 80 |
graph TD
USER[User] -->|HTTP :80| NGINX[Nginx\nProxy Server]
NGINX -->|/api/*| API[Express API\n:8080]
NGINX -->|/* static| UI[React UI\n:3000]
API -->|TCP :6379| REDIS[(Redis\nInventory Cache)]
API -->|TCP :5432| PG[(Postgres\nInventory Prices)]
subgraph "Docker Compose Network"
NGINX
UI
API
REDIS
PG
end
4.2 Adding the Postgres Container
version: '3'
services:
redis-server:
image: redis
postgres:
image: postgres
environment:
POSTGRES_PASSWORD: mysecretpassword
POSTGRES_USER: postgres
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
node-app:
build:
context: ./server
container_name: api-server
ports:
- "8080:8080"
depends_on:
- redis-server
- postgres
ui:
build:
context: ./frontend
ports:
- "3000:3000"
stdin_open: true
volumes:
- ./frontend:/app
- /app/node_modules
proxy:
build:
context: ./proxy
ports:
- "80:80"
depends_on:
- ui
- node-app
SQL initialization script (init.sql):
CREATE TABLE IF NOT EXISTS inventory (
id SERIAL PRIMARY KEY,
amount DECIMAL(10, 2) DEFAULT 0
);
INSERT INTO inventory (amount) VALUES (0);
Postgres connection from Express:
const { Pool } = require('pg');
const cors = require('cors');
const pool = new Pool({
host: 'postgres', // Service name in docker-compose.yml
user: 'postgres',
password: 'mysecretpassword',
database: 'postgres',
port: 5432
});
app.use(cors()); // Required for cross-origin requests from React
4.3 Routing with Nginx
Nginx Dockerfile:
FROM nginx
COPY ./default.conf /etc/nginx/conf.d/default.conf
Nginx configuration (default.conf):
upstream ui {
server ui:3000;
}
upstream api {
server node-app:8080;
}
server {
listen 80;
location /api {
proxy_pass http://api;
}
location / {
proxy_pass http://ui;
}
# WebSocket support for React hot reloading
location /sockjs-node {
proxy_pass http://ui;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
4.4 Restart Policies
services:
node-app:
build: ./server
restart: on-failure
# Options:
# "no" - Never restart (default)
# "always" - Always restart
# "on-failure" - Restart only if exit code != 0
# "unless-stopped" - Always except manual stop
5. Debugging and Production
5.1 Debug Configuration in VS Code
Modify package.json in the Express server:
{
"scripts": {
"start": "node index.js",
"debug": "node --inspect=0.0.0.0:9229 index.js"
}
}
--inspect=0.0.0.0:9229: By default, inspector listens on127.0.0.1(inaccessible from host). Change to0.0.0.0to allow external connections. Never do this in production.
Expose debug port in docker-compose.yml:
services:
node-app:
build: ./server
ports:
- "8080:8080"
- "9229:9229" # Node.js inspector port
.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach to Docker",
"remoteRoot": "/app",
"localRoot": "${workspaceFolder}/server",
"port": 9229,
"address": "localhost",
"restart": true
}
]
}
5.2 Debugging Session
docker-compose up --build— start containers in debug mode- In VS Code: Run → Start Debugging (F5)
- Set breakpoints in
index.js - Trigger the action from the React interface
| Breakpoint Type | Description | Use Case |
|---|---|---|
| Breakpoint | Unconditional stop | General flow inspection |
| Conditional Breakpoint | Stop if expression is true | Error at 1000th item: count > 1000 |
| Logpoint | Log without stopping | Non-intrusive production-like debugging |
5.3 Multi-stage Build for Production
# STAGE 1: Build React application
FROM node:alpine AS build-stage
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
# Compile React → /app/build/ (optimized HTML, JS, CSS)
RUN npm run build
# STAGE 2: Production Nginx server
FROM nginx
# Copy compiled assets to Nginx directory
COPY --from=build-stage /app/build /usr/share/nginx/html
Advantages of multi-stage build:
- Final image contains only Nginx + compiled assets
- Node.js, npm, and all
devDependenciesare excluded - Dramatic image size reduction (~1.2 GB → ~130 MB)
docker build -t myapp_prod .
docker run -p 80:80 myapp_prod
5.4 Docker Networking — Understanding Drivers
| Network Driver | Description | When to Use |
|---|---|---|
| bridge | Default for standalone containers | Single-host container communication |
| host | Container uses host network directly | Performance-critical scenarios |
| overlay | Multi-host networking (Swarm/K8s) | Distributed applications |
| macvlan | Container gets MAC address on LAN | Legacy app integration |
| none | No networking | Security-isolated containers |
docker network ls
docker network inspect bridge
docker network create --driver bridge custom-net
6. Reference Tables
Dockerfile Instructions
| Instruction | Description | Example |
|---|---|---|
FROM | Base image | FROM node:alpine |
WORKDIR | Set working directory | WORKDIR /app |
COPY | Copy files to container | COPY package.json . |
RUN | Execute command (creates layer) | RUN npm install |
CMD | Default start command | CMD ["npm", "start"] |
EXPOSE | Document port (informational) | EXPOSE 3000 |
ENV | Set environment variable | ENV NODE_ENV=production |
ARG | Build-time variable | ARG VERSION=1.0 |
AS | Stage name (multi-stage) | FROM node AS build |
Docker Compose Service Properties
| Property | Description | Example |
|---|---|---|
image | Official Docker Hub image | image: redis |
build | Build from Dockerfile | build: ./frontend |
ports | Port mapping | - "3000:3000" |
volumes | Volume mounts | - ./src:/app/src |
environment | Environment variables | POSTGRES_PASSWORD: secret |
depends_on | Service startup order | depends_on: - redis |
restart | Restart policy | restart: on-failure |
container_name | Custom container name | container_name: api-server |
stdin_open | Keep stdin open | stdin_open: true |
networks | Custom networks | networks: - app-net |
7. Practical Scenarios
Scenario 1 — Disk Full Despite Deleting node_modules
Situation: A developer using Docker Compose finds their disk is full. They deleted node_modules but didn’t recover much space.
Correct answer:
docker-compose down
docker volume prune
docker image prune
docker-compose stop stops containers but doesn’t remove images. docker-compose down removes containers and networks. docker volume prune removes dangling volumes (main disk space culprits).
Scenario 2 — High Availability for a Customer-Facing Application
Situation: Management requires high availability. Services must not fail due to minor bugs.
Correct answer: restart: on-failure
on-failure restarts the container only if the application exits with an error code (exit code ≠ 0). This protects against unexpected crashes while allowing intentional stops (maintenance, deployment) without automatic restart.
services:
node-app:
restart: on-failure
Search Terms
react.js · developing · apps · docker · containerization · containers · kubernetes · application · compose · react · architecture · multi-container · debugging · disk · dockerfile · networking · postgres · production · scenario