# Stage 1: Build
FROM node:20-alpine AS builder WORKDIR /app # Copy package files first — Docker caches this layer
# Only re-runs -weight: 500;">npm ci when package*.json changes
COPY package*.json ./
RUN -weight: 500;">npm ci --only=production # Stage 2: Run
FROM node:20-alpine AS runner WORKDIR /app # Security: run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup # Copy only production dependencies from builder stage
COPY --from=builder /app/node_modules ./node_modules # Copy application code
COPY . . # Switch to non-root user before starting
USER appuser EXPOSE 3000 CMD ["node", "server.js"]
# Stage 1: Build
FROM node:20-alpine AS builder WORKDIR /app # Copy package files first — Docker caches this layer
# Only re-runs -weight: 500;">npm ci when package*.json changes
COPY package*.json ./
RUN -weight: 500;">npm ci --only=production # Stage 2: Run
FROM node:20-alpine AS runner WORKDIR /app # Security: run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup # Copy only production dependencies from builder stage
COPY --from=builder /app/node_modules ./node_modules # Copy application code
COPY . . # Switch to non-root user before starting
USER appuser EXPOSE 3000 CMD ["node", "server.js"]
# Stage 1: Build
FROM node:20-alpine AS builder WORKDIR /app # Copy package files first — Docker caches this layer
# Only re-runs -weight: 500;">npm ci when package*.json changes
COPY package*.json ./
RUN -weight: 500;">npm ci --only=production # Stage 2: Run
FROM node:20-alpine AS runner WORKDIR /app # Security: run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup # Copy only production dependencies from builder stage
COPY --from=builder /app/node_modules ./node_modules # Copy application code
COPY . . # Switch to non-root user before starting
USER appuser EXPOSE 3000 CMD ["node", "server.js"]
node_modules
.-weight: 500;">git
.gitignore
*.md
.env
.env.*
dist
coverage
.nyc_output
logs
*.log
node_modules
.-weight: 500;">git
.gitignore
*.md
.env
.env.*
dist
coverage
.nyc_output
logs
*.log
node_modules
.-weight: 500;">git
.gitignore
*.md
.env
.env.*
dist
coverage
.nyc_output
logs
*.log
# -weight: 500;">docker-compose.yml
version: '3.8' services: api: build: . ports: - "3000:3000" environment: - NODE_ENV=development - MONGO_URI=mongodb://mongo:27017/appdb - JWT_SECRET=local_dev_secret volumes: # Hot reload: mount source code - .:/app # Prevent local node_modules from overriding container's - /app/node_modules depends_on: mongo: condition: service_healthy -weight: 500;">restart: unless-stopped mongo: image: mongo:7 ports: - "27017:27017" volumes: - mongo_data:/data/db healthcheck: test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] interval: 10s timeout: 5s retries: 5 volumes: mongo_data:
# -weight: 500;">docker-compose.yml
version: '3.8' services: api: build: . ports: - "3000:3000" environment: - NODE_ENV=development - MONGO_URI=mongodb://mongo:27017/appdb - JWT_SECRET=local_dev_secret volumes: # Hot reload: mount source code - .:/app # Prevent local node_modules from overriding container's - /app/node_modules depends_on: mongo: condition: service_healthy -weight: 500;">restart: unless-stopped mongo: image: mongo:7 ports: - "27017:27017" volumes: - mongo_data:/data/db healthcheck: test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] interval: 10s timeout: 5s retries: 5 volumes: mongo_data:
# -weight: 500;">docker-compose.yml
version: '3.8' services: api: build: . ports: - "3000:3000" environment: - NODE_ENV=development - MONGO_URI=mongodb://mongo:27017/appdb - JWT_SECRET=local_dev_secret volumes: # Hot reload: mount source code - .:/app # Prevent local node_modules from overriding container's - /app/node_modules depends_on: mongo: condition: service_healthy -weight: 500;">restart: unless-stopped mongo: image: mongo:7 ports: - "27017:27017" volumes: - mongo_data:/data/db healthcheck: test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] interval: 10s timeout: 5s retries: 5 volumes: mongo_data:
-weight: 500;">docker-compose up # foreground
-weight: 500;">docker-compose up -d # background
-weight: 500;">docker-compose down # -weight: 500;">stop and -weight: 500;">remove containers
-weight: 500;">docker-compose down -v # also -weight: 500;">remove volumes (wipes database)
-weight: 500;">docker-compose up # foreground
-weight: 500;">docker-compose up -d # background
-weight: 500;">docker-compose down # -weight: 500;">stop and -weight: 500;">remove containers
-weight: 500;">docker-compose down -v # also -weight: 500;">remove volumes (wipes database)
-weight: 500;">docker-compose up # foreground
-weight: 500;">docker-compose up -d # background
-weight: 500;">docker-compose down # -weight: 500;">stop and -weight: 500;">remove containers
-weight: 500;">docker-compose down -v # also -weight: 500;">remove volumes (wipes database)
Inbound: Port 22 (SSH) — Your IP only Port 80 (HTTP) — 0.0.0.0/0 Port 443 (HTTPS) — 0.0.0.0/0 Outbound: All traffic — 0.0.0.0/0
Inbound: Port 22 (SSH) — Your IP only Port 80 (HTTP) — 0.0.0.0/0 Port 443 (HTTPS) — 0.0.0.0/0 Outbound: All traffic — 0.0.0.0/0
Inbound: Port 22 (SSH) — Your IP only Port 80 (HTTP) — 0.0.0.0/0 Port 443 (HTTPS) — 0.0.0.0/0 Outbound: All traffic — 0.0.0.0/0
# Update system
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">update && -weight: 600;">sudo -weight: 500;">apt -weight: 500;">upgrade -y # Install Docker
-weight: 500;">curl -fsSL https://get.-weight: 500;">docker.com -o get--weight: 500;">docker.sh
-weight: 600;">sudo sh get--weight: 500;">docker.sh
-weight: 600;">sudo usermod -aG -weight: 500;">docker ubuntu
newgrp -weight: 500;">docker # Install Docker Compose
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -weight: 500;">docker-compose-plugin -y # Install Nginx
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install nginx -y # Install Certbot for SSL
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install certbot python3-certbot-nginx -y # Create app directory
mkdir -p /home/ubuntu/app
# Update system
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">update && -weight: 600;">sudo -weight: 500;">apt -weight: 500;">upgrade -y # Install Docker
-weight: 500;">curl -fsSL https://get.-weight: 500;">docker.com -o get--weight: 500;">docker.sh
-weight: 600;">sudo sh get--weight: 500;">docker.sh
-weight: 600;">sudo usermod -aG -weight: 500;">docker ubuntu
newgrp -weight: 500;">docker # Install Docker Compose
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -weight: 500;">docker-compose-plugin -y # Install Nginx
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install nginx -y # Install Certbot for SSL
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install certbot python3-certbot-nginx -y # Create app directory
mkdir -p /home/ubuntu/app
# Update system
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">update && -weight: 600;">sudo -weight: 500;">apt -weight: 500;">upgrade -y # Install Docker
-weight: 500;">curl -fsSL https://get.-weight: 500;">docker.com -o get--weight: 500;">docker.sh
-weight: 600;">sudo sh get--weight: 500;">docker.sh
-weight: 600;">sudo usermod -aG -weight: 500;">docker ubuntu
newgrp -weight: 500;">docker # Install Docker Compose
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -weight: 500;">docker-compose-plugin -y # Install Nginx
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install nginx -y # Install Certbot for SSL
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install certbot python3-certbot-nginx -y # Create app directory
mkdir -p /home/ubuntu/app
# /etc/nginx/sites-available/api
server { listen 80; server_name api.yourdomain.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection '-weight: 500;">upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; }
}
# /etc/nginx/sites-available/api
server { listen 80; server_name api.yourdomain.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection '-weight: 500;">upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; }
}
# /etc/nginx/sites-available/api
server { listen 80; server_name api.yourdomain.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection '-weight: 500;">upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; }
}
# Enable the site
-weight: 600;">sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/
-weight: 600;">sudo nginx -t
-weight: 600;">sudo -weight: 500;">systemctl reload nginx # Get SSL certificate (replace with your domain)
-weight: 600;">sudo certbot --nginx -d api.yourdomain.com
# Enable the site
-weight: 600;">sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/
-weight: 600;">sudo nginx -t
-weight: 600;">sudo -weight: 500;">systemctl reload nginx # Get SSL certificate (replace with your domain)
-weight: 600;">sudo certbot --nginx -d api.yourdomain.com
# Enable the site
-weight: 600;">sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/
-weight: 600;">sudo nginx -t
-weight: 600;">sudo -weight: 500;">systemctl reload nginx # Get SSL certificate (replace with your domain)
-weight: 600;">sudo certbot --nginx -d api.yourdomain.com
# .github/workflows/deploy.yml
name: Deploy to EC2 on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Deploy to EC2 uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.EC2_HOST }} username: ubuntu key: ${{ secrets.EC2_SSH_KEY }} script: | cd /home/ubuntu/app # Pull latest code -weight: 500;">git pull origin main # Build new image -weight: 500;">docker build -t myapp:latest . # Zero-downtime: -weight: 500;">start new container, -weight: 500;">stop old one -weight: 500;">docker compose up -d --build --no-deps api # Clean up old images -weight: 500;">docker image prune -f
# .github/workflows/deploy.yml
name: Deploy to EC2 on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Deploy to EC2 uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.EC2_HOST }} username: ubuntu key: ${{ secrets.EC2_SSH_KEY }} script: | cd /home/ubuntu/app # Pull latest code -weight: 500;">git pull origin main # Build new image -weight: 500;">docker build -t myapp:latest . # Zero-downtime: -weight: 500;">start new container, -weight: 500;">stop old one -weight: 500;">docker compose up -d --build --no-deps api # Clean up old images -weight: 500;">docker image prune -f
# .github/workflows/deploy.yml
name: Deploy to EC2 on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Deploy to EC2 uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.EC2_HOST }} username: ubuntu key: ${{ secrets.EC2_SSH_KEY }} script: | cd /home/ubuntu/app # Pull latest code -weight: 500;">git pull origin main # Build new image -weight: 500;">docker build -t myapp:latest . # Zero-downtime: -weight: 500;">start new container, -weight: 500;">stop old one -weight: 500;">docker compose up -d --build --no-deps api # Clean up old images -weight: 500;">docker image prune -f
# -weight: 500;">docker-compose.yml — two app instances
services: api-blue: build: . ports: - "3001:3000" api-green: build: . ports: - "3002:3000"
# -weight: 500;">docker-compose.yml — two app instances
services: api-blue: build: . ports: - "3001:3000" api-green: build: . ports: - "3002:3000"
# -weight: 500;">docker-compose.yml — two app instances
services: api-blue: build: . ports: - "3001:3000" api-green: build: . ports: - "3002:3000"
# deploy.sh on EC2
#!/bin/bash # Determine current active color
ACTIVE=$(cat /tmp/active_color 2>/dev/null || echo "blue")
NEXT=$([ "$ACTIVE" = "blue" ] && echo "green" || echo "blue") # Start new version
-weight: 500;">docker compose up -d --build api-$NEXT # Wait for health check
sleep 10 # Switch Nginx to new version
PORT=$([ "$NEXT" = "blue" ] && echo "3001" || echo "3002")
sed -i "s/proxy_pass http:\/\/localhost:[0-9]*/proxy_pass http:\/\/localhost:$PORT/" /etc/nginx/sites-available/api
nginx -s reload # Stop old version
-weight: 500;">docker compose -weight: 500;">stop api-$ACTIVE # Record active color
echo $NEXT > /tmp/active_color
# deploy.sh on EC2
#!/bin/bash # Determine current active color
ACTIVE=$(cat /tmp/active_color 2>/dev/null || echo "blue")
NEXT=$([ "$ACTIVE" = "blue" ] && echo "green" || echo "blue") # Start new version
-weight: 500;">docker compose up -d --build api-$NEXT # Wait for health check
sleep 10 # Switch Nginx to new version
PORT=$([ "$NEXT" = "blue" ] && echo "3001" || echo "3002")
sed -i "s/proxy_pass http:\/\/localhost:[0-9]*/proxy_pass http:\/\/localhost:$PORT/" /etc/nginx/sites-available/api
nginx -s reload # Stop old version
-weight: 500;">docker compose -weight: 500;">stop api-$ACTIVE # Record active color
echo $NEXT > /tmp/active_color
# deploy.sh on EC2
#!/bin/bash # Determine current active color
ACTIVE=$(cat /tmp/active_color 2>/dev/null || echo "blue")
NEXT=$([ "$ACTIVE" = "blue" ] && echo "green" || echo "blue") # Start new version
-weight: 500;">docker compose up -d --build api-$NEXT # Wait for health check
sleep 10 # Switch Nginx to new version
PORT=$([ "$NEXT" = "blue" ] && echo "3001" || echo "3002")
sed -i "s/proxy_pass http:\/\/localhost:[0-9]*/proxy_pass http:\/\/localhost:$PORT/" /etc/nginx/sites-available/api
nginx -s reload # Stop old version
-weight: 500;">docker compose -weight: 500;">stop api-$ACTIVE # Record active color
echo $NEXT > /tmp/active_color
# /home/ubuntu/app/.env (on EC2, not in -weight: 500;">git)
NODE_ENV=production
MONGO_URI=mongodb://mongo:27017/appdb
JWT_SECRET=your_actual_secret_here
STRIPE_SECRET_KEY=sk_live_...
# /home/ubuntu/app/.env (on EC2, not in -weight: 500;">git)
NODE_ENV=production
MONGO_URI=mongodb://mongo:27017/appdb
JWT_SECRET=your_actual_secret_here
STRIPE_SECRET_KEY=sk_live_...
# /home/ubuntu/app/.env (on EC2, not in -weight: 500;">git)
NODE_ENV=production
MONGO_URI=mongodb://mongo:27017/appdb
JWT_SECRET=your_actual_secret_here
STRIPE_SECRET_KEY=sk_live_...
services: api: env_file: - .env
services: api: env_file: - .env
services: api: env_file: - .env
# View live logs
-weight: 500;">docker compose logs -f api # Container resource usage
-weight: 500;">docker stats # Check if containers are running
-weight: 500;">docker compose ps
# View live logs
-weight: 500;">docker compose logs -f api # Container resource usage
-weight: 500;">docker stats # Check if containers are running
-weight: 500;">docker compose ps
# View live logs
-weight: 500;">docker compose logs -f api # Container resource usage
-weight: 500;">docker stats # Check if containers are running
-weight: 500;">docker compose ps - A multi-stage Docker build that produces a small, secure image
- -weight: 500;">docker-compose for local development with MongoDB included
- GitHub Actions CI/CD that deploys to EC2 on every push to main
- Nginx as a reverse proxy with SSL via Let's Encrypt
- Zero-downtime deployments - t3.small (2GB RAM) — minimum for a real Node.js API
- t3.medium (4GB RAM) — recommended for APIs with meaningful traffic
- t2.micro — the free tier option, too slow for production under any real load - EC2_HOST — your EC2 public IP
- EC2_SSH_KEY — your EC2 private key (the .pem file contents) - [ ] Elastic IP assigned (so IP doesn't change on -weight: 500;">restart)
- [ ] Domain pointing to Elastic IP
- [ ] SSL certificate installed (Certbot)
- [ ] Security group only exposes 80, 443, 22
- [ ] .env file on server with production secrets
- [ ] -weight: 500;">docker-compose -weight: 500;">restart: unless-stopped set on all services
- [ ] GitHub Actions secrets set
- [ ] First deployment tested manually