Section 01
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
- Apache runs on the host
- Node apps run only inside Docker
- Docker replaces PM2 (one process per container)
- SSL terminates at Apache (Let's Encrypt)
- Each app = one Docker image + container
- Containers communicate via ports, not public exposure
:: Critical Rule
Apache is the only service exposed on ports 80/443. Docker containers
bind to 127.0.0.1:<port> internally.
Section 02
EC2 Initial Setup (One-Time)
Update System
Bash
sudo apt update && sudo apt upgrade -yInstall Apache2
Bash
sudo apt install apache2 -y
sudo systemctl enable apache2
sudo systemctl start apache2Install 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 -yDocker without sudo (IMPORTANT)
Bash
sudo usermod -aG docker $USER
newgrp dockerVerify Installation
Bash
docker version
docker compose versionSection 03
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.jsRequired Files
package.jsonpackage-lock.json(mandatory fornpm ci)src/index.js(or your app entry point)
:: Never Run npm on Host
npm install happens only inside Docker. The lockfile is generated
inside Docker only.
Section 04
Node.js Base Image Decision
Why Not Alpine?
- Alpine lacks
glibc - Native modules break easily
- Debugging is harder
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
Section 05
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
npm cifor deterministic installs- Production-only dependencies
- Clean, reproducible builds
- No PM2 required
Section 06
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: developmentDev Behavior
- Code is mounted via volumes
nodemonrestarts app automatically on changes- NO need to re-run docker compose on code changes
- Port bound to localhost only
Start Development Mode
Bash
docker compose -f docker-compose.dev.yml up --buildProduction: 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: alwaysProd Behavior
- No volumes (immutable image)
- Changes require rebuild
- Automatic restart on failure
- Port bound to localhost only
Deploy/Update Production
Bash
docker compose down
docker compose up -d --buildSection 07
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.jsonchanges (dependencies)- Dockerfile changes
.dockerignorechanges- Production code updates
Section 08
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 upLogs appear directly in the terminal.
Background Mode
Bash
docker compose up -d
docker logs -f task-manager-api-prodUse -f flag to follow logs in real-time.
Best Practice
Bash
docker compose logs -fShows 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
Section 09
Apache Reverse Proxy
Enable Required Modules
Bash
sudo a2enmod proxy proxy_http ssl rewrite
sudo systemctl restart apache2VirtualHost 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
Section 10
SSL Setup (Let's Encrypt)
Install Certbot
Bash
sudo apt install certbot python3-certbot-apache -yGenerate SSL Certificate
Bash
sudo certbot --apache -d api.example.comAuto-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
Section 11
Multiple Apps on Same EC2
Pattern
- New Docker image per app
- New container per app
- New internal port per app
- New Apache VirtualHost per app
Port Assignment Example
App A
Domain:
Port:
api.example.comPort:
localhost:3000
App B
Domain:
Port:
dashboard.example.comPort:
localhost:3001
App C
Domain:
Port:
admin.example.comPort:
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 |
Section 12
Final Rules (Non-Negotiable)
- Never run
npm installon EC2 host - Docker replaces PM2 completely
- One app = one container
- Apache is the single public entry point
- SSL terminates at Apache
- Dev uses volumes + nodemon
- Prod uses immutable images
- Always use
npm ciin Docker
:: This Setup Provides
- Production-safe deployments
- Scalable architecture
- Operationally clean
- Reproducible builds
- Zero dependency conflicts
Result
Final Outcome
Containerized
Reproducible
Production Parity
Secure
CI/CD Ready
Cloud Compatible