~/production-guide

const DockerNodeEC2 = { }

// Complete Production Deployment Guide

Docker Node.js 20 EC2 Ubuntu Apache2 SSL/TLS Multi-App

High-Level Architecture

Internet
   
(ALB or Elastic IP)
   
Apache2 (Host)
   
Reverse Proxy (VirtualHosts)
   
Docker Containers (Multiple Apps)
   
Node.js App (Port 3000, 3001, etc.)

Key Principles

:: Critical Rule

Apache is the only service exposed on ports 80/443. Docker containers bind to 127.0.0.1:<port> internally.

EC2 Initial Setup (One-Time)

Update System

Bash
sudo apt update && sudo apt upgrade -y

Install Apache2

Bash
sudo apt install apache2 -y sudo systemctl enable apache2 sudo systemctl start apache2

Install Docker Engine (REQUIRED)

:: Important

Yes — Docker must be installed on EC2. This is not optional.

Bash
sudo apt install ca-certificates curl gnupg -y sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y

Docker without sudo (IMPORTANT)

Bash
sudo usermod -aG docker $USER newgrp docker

Verify Installation

Bash
docker version docker compose version

Project Structure

Per App Structure

Tree
task-manager-apis/ ├── Dockerfile ├── docker-compose.dev.yml ├── docker-compose.yml (production) ├── package.json ├── package-lock.json └── src/ └── index.js

Required Files

:: Never Run npm on Host

npm install happens only inside Docker. The lockfile is generated inside Docker only.

Node.js Base Image Decision

Why Not Alpine?

Recommended Base Image

Docker
node:20-bookworm-slim
:: Why This Image?
  • Uses glibc (not musl like Alpine)
  • Compatible with native npm dependencies
  • Stable on EC2
  • Smaller than full Debian, safer than Alpine

Dockerfile (Production-Ready)

Dockerfile
FROM node:20-bookworm-slim WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . EXPOSE 3000 CMD ["node", "src/index.js"]

Key Features

Docker Compose Configurations

Development: docker-compose.dev.yml

YAML
version: "3.9" services: task-manager-api: image: task-manager-api:dev container_name: task-manager-api-dev build: context: . dockerfile: Dockerfile command: npx nodemon src/index.js ports: - "127.0.0.1:3000:3000" volumes: - .:/app - /app/node_modules environment: NODE_ENV: development

Dev Behavior

Start Development Mode

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

Production: docker-compose.yml

YAML
version: "3.9" services: task-manager-api: image: task-manager-api:prod container_name: task-manager-api-prod build: context: . dockerfile: Dockerfile ports: - "127.0.0.1:3000:3000" environment: NODE_ENV: production restart: always

Prod Behavior

Deploy/Update Production

Bash
docker compose down docker compose up -d --build

Code Change Behavior

Mode Change Code Auto Reload? Command Needed
Dev (volumes + nodemon) Yes ✅ Yes ❌ None
Production Yes ❌ No ✅ Rebuild required
:: When Rebuild is Required

Re-run docker compose up --build only if:

  • package.json changes (dependencies)
  • Dockerfile changes
  • .dockerignore changes
  • Production code updates

Console Logs (Critical)

Does console.log() Show in Terminal?

:: Yes!

Docker captures stdout/stderr automatically. Your console.log() statements will appear in the terminal.

Foreground Mode

Bash
docker compose up

Logs appear directly in the terminal.

Background Mode

Bash
docker compose up -d docker logs -f task-manager-api-prod

Use -f flag to follow logs in real-time.

Best Practice

Bash
docker compose logs -f

Shows logs for all services defined in docker-compose.yml

:: Important Note
  • Docker captures stdout/stderr automatically
  • No extra logging setup needed
  • Apache, SSL, ALB, or Elastic IP do not affect logs
  • Logs are container-specific, not system-wide

Apache Reverse Proxy

Enable Required Modules

Bash
sudo a2enmod proxy proxy_http ssl rewrite sudo systemctl restart apache2

VirtualHost Configuration

Create: /etc/apache2/sites-available/api.example.com.conf

Apache Config
<VirtualHost *:80> ServerName api.example.com ProxyPreserveHost On ProxyPass / http://localhost:3000/ ProxyPassReverse / http://localhost:3000/ ErrorLog ${APACHE_LOG_DIR}/api-error.log CustomLog ${APACHE_LOG_DIR}/api-access.log combined </VirtualHost>

Enable Site

Bash
sudo a2ensite api.example.com.conf sudo systemctl reload apache2
:: Apache Responsibilities
  • SSL termination (Let's Encrypt)
  • Reverse proxy to Docker containers
  • VirtualHost per app/domain
  • No Node.js or PM2 involvement

SSL Setup (Let's Encrypt)

Install Certbot

Bash
sudo apt install certbot python3-certbot-apache -y

Generate SSL Certificate

Bash
sudo certbot --apache -d api.example.com

Auto-Renewal

Bash
sudo certbot renew --dry-run
:: SSL Behavior
  • Apache handles SSL — containers stay HTTP-only
  • Certbot auto-configures Apache
  • Auto-renewal happens automatically via cron

Multiple Apps on Same EC2

Pattern

Port Assignment Example

App A
Domain: api.example.com
Port: localhost:3000
App B
Domain: dashboard.example.com
Port: localhost:3001
App C
Domain: admin.example.com
Port: localhost:3002

What Docker Replaces

Previously Now with Docker
PM2 Docker restart policy
Manual restarts docker compose up -d
Host Node.js Containerized Node
Port conflicts Explicit bindings
Dependency conflicts Isolated environments

Final Rules (Non-Negotiable)

  1. Never run npm install on EC2 host
  2. Docker replaces PM2 completely
  3. One app = one container
  4. Apache is the single public entry point
  5. SSL terminates at Apache
  6. Dev uses volumes + nodemon
  7. Prod uses immutable images
  8. Always use npm ci in Docker
:: This Setup Provides
  • Production-safe deployments
  • Scalable architecture
  • Operationally clean
  • Reproducible builds
  • Zero dependency conflicts

Final Outcome

🐳
Containerized
🔄
Reproducible
🎯
Production Parity
🔐
Secure
🚀
CI/CD Ready
☁️
Cloud Compatible