Intermediate

Developing React.js Apps with Docker

Dockerize a React app, run a multi-container setup with Compose and prepare it for production.

Level: Intermediate
Demo application: Globomantics — e-commerce portal refactored into microservices


Table of Contents

  1. Course Overview
  2. Dockerizing a Basic React Application
  3. Multi-container Application with Docker Compose
  4. Improving the Multi-container Application
  5. Debugging and Production
  6. Reference Tables
  7. 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 Dockerfile components 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.json separately?
Docker caches each layer. If package.json hasn’t changed, Docker reuses the npm install cache 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

ComponentRoleDocker Image
React UIUser interfaceNode:alpine (custom)
Express APIREST API serverNode:alpine (custom)
RedisKey-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.yml automatically 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_modules is crucial: it tells Docker not to overwrite the container’s node_modules with the host’s (which may not exist).

3.5 Essential Docker Compose Commands

CommandDescription
docker-compose upStart all services (foreground)
docker-compose up -dStart in detached mode (background)
docker-compose up --buildRebuild + start
docker-compose downStop and remove containers + networks
docker-compose stopStop containers (without removal)
docker-compose logs <service>View service logs
docker-compose psStatus of all containers + ports
docker-compose imagesList Docker Compose images + sizes

stop vs down: stop halts processes but keeps images and volumes. down also 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

ComponentRolePort
React UIInterface (Add/Check/Sell)3000
Express APIREST server8080
RedisCache — inventory counter6379
PostgresRelational DB — inventory totals5432
NginxProxy server — traffic routing80
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 on 127.0.0.1 (inaccessible from host). Change to 0.0.0.0 to 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

  1. docker-compose up --build — start containers in debug mode
  2. In VS Code: Run → Start Debugging (F5)
  3. Set breakpoints in index.js
  4. Trigger the action from the React interface
Breakpoint TypeDescriptionUse Case
BreakpointUnconditional stopGeneral flow inspection
Conditional BreakpointStop if expression is trueError at 1000th item: count > 1000
LogpointLog without stoppingNon-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 devDependencies are 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 DriverDescriptionWhen to Use
bridgeDefault for standalone containersSingle-host container communication
hostContainer uses host network directlyPerformance-critical scenarios
overlayMulti-host networking (Swarm/K8s)Distributed applications
macvlanContainer gets MAC address on LANLegacy app integration
noneNo networkingSecurity-isolated containers
docker network ls
docker network inspect bridge
docker network create --driver bridge custom-net

6. Reference Tables

Dockerfile Instructions

InstructionDescriptionExample
FROMBase imageFROM node:alpine
WORKDIRSet working directoryWORKDIR /app
COPYCopy files to containerCOPY package.json .
RUNExecute command (creates layer)RUN npm install
CMDDefault start commandCMD ["npm", "start"]
EXPOSEDocument port (informational)EXPOSE 3000
ENVSet environment variableENV NODE_ENV=production
ARGBuild-time variableARG VERSION=1.0
ASStage name (multi-stage)FROM node AS build

Docker Compose Service Properties

PropertyDescriptionExample
imageOfficial Docker Hub imageimage: redis
buildBuild from Dockerfilebuild: ./frontend
portsPort mapping- "3000:3000"
volumesVolume mounts- ./src:/app/src
environmentEnvironment variablesPOSTGRES_PASSWORD: secret
depends_onService startup orderdepends_on: - redis
restartRestart policyrestart: on-failure
container_nameCustom container namecontainer_name: api-server
stdin_openKeep stdin openstdin_open: true
networksCustom networksnetworks: - 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.