Back to Writing

Essay

Dockerizing a React and Node.js Full-Stack App

A comprehensive guide to containerizing full-stack applications with React frontend and Node.js backend using Docker and Docker Compose.

December 19, 20245 min readDockerReactNode.js

Introduction

Docker allows you to package applications with all dependencies, ensuring consistent behavior across environments. In this guide, we'll dockerize a full-stack app with a React frontend and Node.js backend, making deployment and scaling much easier.

Prerequisites

You'll need:

  • Docker installed locally
  • Docker Compose installed
  • Basic knowledge of React and Node.js
  • A full-stack project with separate frontend and backend directories

Project Structure

Before we start, ensure your project has this structure:

fullstack-app/
├── backend/
│   ├── package.json
│   ├── server.js
│   └── Dockerfile
├── frontend/
│   ├── package.json
│   ├── src/
│   └── Dockerfile
├── docker-compose.yml
└── .dockerignore

Step 1: Create Dockerfile for Backend

Inside /backend/Dockerfile:

# Use Node.js 18 Alpine for smaller image size
FROM node:18-alpine

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Expose port
EXPOSE 5000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:5000/health || exit 1

# Start the application
CMD ["npm", "start"]

Step 2: Create Dockerfile for Frontend

Inside /frontend/Dockerfile:

# Build stage
FROM node:18-alpine AS build

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci

# Copy source code
COPY . .

# Build the application
RUN npm run build

# Production stage
FROM nginx:alpine

# Copy built files to nginx
COPY --from=build /app/build /usr/share/nginx/html

# Copy custom nginx config
COPY nginx.conf /etc/nginx/nginx.conf

# Expose port
EXPOSE 3000

# Start nginx
CMD ["nginx", "-g", "daemon off;"]

Step 3: Create Nginx Configuration for Frontend

Create frontend/nginx.conf:

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    server {
        listen 3000;
        server_name localhost;
        root /usr/share/nginx/html;
        index index.html;

        # Handle React Router
        location / {
            try_files $uri $uri/ /index.html;
        }

        # Cache static assets
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }

        # Gzip compression
        gzip on;
        gzip_vary on;
        gzip_min_length 1024;
        gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
    }
}

Step 4: Docker Compose Setup

Create docker-compose.yml in the root directory:

version: '3.8'

services:
  backend:
    build: ./backend
    container_name: fullstack-backend
    ports:
      - "5000:5000"
    environment:
      - NODE_ENV=production
      - PORT=5000
    volumes:
      - ./backend:/app
      - /app/node_modules
    networks:
      - fullstack-network
    restart: unless-stopped

  frontend:
    build: ./frontend
    container_name: fullstack-frontend
    ports:
      - "3000:3000"
    depends_on:
      - backend
    networks:
      - fullstack-network
    restart: unless-stopped

  # Optional: Add a database
  # mongodb:
  #   image: mongo:6
  #   container_name: fullstack-mongodb
  #   ports:
  #     - "27017:27017"
  #   volumes:
  #     - mongodb_data:/data/db
  #   networks:
  #     - fullstack-network
  #   restart: unless-stopped

networks:
  fullstack-network:
    driver: bridge

# volumes:
#   mongodb_data:

Step 5: Create .dockerignore Files

Backend .dockerignore

Create backend/.dockerignore:

node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.nyc_output
coverage

Frontend .dockerignore

Create frontend/.dockerignore:

node_modules
npm-debug.log
build
.git
.gitignore
README.md
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

Step 6: Build and Run the App

Build Images

# Build all services
docker-compose build

# Or build specific service
docker-compose build backend
docker-compose build frontend

Run the Application

# Start all services
docker-compose up

# Run in background
docker-compose up -d

# View logs
docker-compose logs -f

# Stop services
docker-compose down

Step 7: Development Workflow

Development Mode

For development, create docker-compose.dev.yml:

version: '3.8'

services:
  backend:
    build: ./backend
    ports:
      - "5000:5000"
    volumes:
      - ./backend:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
    command: npm run dev

  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    volumes:
      - ./frontend:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
    command: npm start

Run Development Environment

docker-compose -f docker-compose.dev.yml up

Step 8: Production Deployment

Production Build

# Build production images
docker-compose -f docker-compose.yml build

# Push to registry (optional)
docker tag fullstack-backend:latest your-registry/backend:latest
docker tag fullstack-frontend:latest your-registry/frontend:latest
docker push your-registry/backend:latest
docker push your-registry/frontend:latest

Production Deployment

# Deploy to production
docker-compose -f docker-compose.prod.yml up -d

# Scale services
docker-compose up -d --scale backend=3

Benefits

  • Consistent environments for dev, staging, and production
  • Simplified deployment with single command
  • Easy scaling with Docker Swarm or Kubernetes
  • Isolated dependencies preventing conflicts
  • Version control for your entire application stack
  • Portability across different environments

Advanced Features

Multi-Stage Builds

Optimize your Dockerfiles with multi-stage builds:

# Example for backend
FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine AS production
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
EXPOSE 5000
CMD ["npm", "start"]

Health Checks

Add health checks to your services:

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s

Troubleshooting

Common Issues

# View container logs
docker-compose logs backend
docker-compose logs frontend

# Access container shell
docker-compose exec backend sh
docker-compose exec frontend sh

# Rebuild specific service
docker-compose build --no-cache backend

# Clean up
docker-compose down -v
docker system prune -a

Performance Optimization

# Use .dockerignore files
# Multi-stage builds
# Alpine base images
# Layer caching optimization

Conclusion

Dockerizing your full-stack app makes deployments faster, cleaner, and more reliable. With proper configuration, you can easily scale your application and maintain consistency across different environments.

Happy containerizing! 🐳