ENGINEERING COMMAND CENTER

LIVE ENGINEERING STATUS
Available for Full-Time Roles
Open to opportunities worldwide
RoleSenior Full Stack Engineer
Focus AreaBackend • Cloud • AI
Experience5+ Years
TimezoneFlexible
CurrentlyActively Looking
← Back to Engineering Articles
Category: DevOps

Deploying Node.js Apps on AWS EC2

1. Why I wrote this

Deploying software manually by SSHing into servers, pulling code, and restarting processes is highly error-prone. This guide outlines how to build a containerized, self-healing, automated CI/CD pipeline that builds, tests, and deploys Node.js applications onto AWS EC2 with zero-downtime Nginx updates.

2. The Problem

Without structured deployment processes, software teams encounter:

  • Server Drift: Production server environments diverge from local machines because of manual library installations.
  • Downtime During Updates: Stopping the Node process to swap files leaves users with 502 Bad Gateway timeouts.
  • Security Hazards: Exposing raw application ports (like Node's 3000) directly to the web invites brute-force attacks.

3. Core Concept

To achieve a reliable, secure deployment, we combine four layers:

  • AWS EC2 Virtual Machine: Acts as our elastic cloud compute host inside a virtual private cloud (VPC).
  • Docker Containers: Package our code, node_modules, and OS variables into immutable runtime images.
  • Nginx Reverse Proxy: Receives HTTP/S requests on ports 80/443, terminates SSL certificates, and forwards requests to local Docker ports.
  • GitHub Actions: Automatically triggers on git-push to build Docker images, test code, upload images, and trigger the remote EC2 server to run the container.

4. How it works

The delivery flow is fully automated: 1. Developer pushes a commit to the main branch. 2. GitHub Actions runner checks out code and runs test blocks. 3. If tests pass, it compiles a multi-stage Docker build and pushes the versioned image to Docker Hub. 4. The workflow runs an SSH deploy script, logging into the EC2 instance using a secure PEM private key. 5. On the EC2 terminal, Docker Compose pulls the updated image, recreates the container, and restarts Nginx to apply proxy routing.

5. Architecture or Flow

The automated CI/CD deployment architecture and network topology are shown below:

text
[ Developer Machine ] --( Git Push Commit )--> [ GitHub Repo ]
                                                     |
                                            ( Triggers Actions Workflow )
                                                     |
                                            [ GitHub Runner Node ]
                                            /                  \
                            ( Build & Push Image )        ( SSH Deploy Cmd )
                                          /                      \
                                  [ Docker Hub ]             [ AWS EC2 VM Instance ]
                                        |                             |
                                        |                             v
                                        |                   [ Docker Compose Hook ]
                                        |                             |
                                        `----( Pulls New Image )------'
                                                                      |
                                                            [ Local Nginx Proxy ]
                                                            /                   \
                                            ( Port 80/443 Traffic )      ( Port 3000 App )
                                                            |                   |
[ Client Browser ] <====( SSL TLS HTTPS Secure Web Traffic )'                   `-[ Node API Docker ]

6. Code Example

Below is an optimized, multi-stage production Dockerfile and the corresponding GitHub Actions deploy script:

dockerfile
# --- Stage 1: Build & Dependencies ---
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .

# --- Stage 2: Minimal Runtime ---
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/src ./src

EXPOSE 3000
USER node
CMD ["node", "src/index.js"]

Below is the corresponding GitHub Actions pipeline configuration (.github/workflows/deploy.yml):

yaml
name: Production EC2 Deployment

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and Push Docker Image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ secrets.DOCKER_USERNAME }}/node-app:latest

      - name: Deploy to AWS EC2 via SSH
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          script: |
            docker pull ${{ secrets.DOCKER_USERNAME }}/node-app:latest
            docker-compose down
            docker-compose up -d
            docker system prune -f

7. Security or Performance Considerations

Important
VPC Port Hardening: Never open ports 3000, 5432, or 27017 on your AWS Security Group. Keep all internal application ports blocked from the outside. Only expose: - Port 80 (HTTP): Expose to let Nginx redirect traffic to SSL. - Port 443 (HTTPS): Expose for all secure production traffic. - Port 22 (SSH): Limit incoming access to your specific office IP addresses to block script bots.
  • Non-Root Docker execution: By default, commands inside containers run as the root user. If an attacker breaches your Node code, they gain root access to the host machine. Adding USER node inside our Dockerfile ensures the process runs under limited privileges, preventing container escape attacks.

8. Common Mistakes

  • Hardcoding Environment Secrets: Storing credentials like database URIs or private keys inside repositories is a major security risk. Inject these variables into the container environment at runtime using Docker Compose files backed by AWS Parameter Store keys.
  • Neglecting Auto-Restart Rules: If the container runs out of memory, it dies. Always declare restart: always or restart: unless-stopped inside your Compose file to tell Docker to automatically spin the application back up.

9. Where I applied it

I implemented this CI/CD infrastructure in awsdeployment. The pipeline securely deploys Node.js docker images to an AWS EC2 instance, utilizing Nginx to resolve custom domain routes and automatically renew Let's Encrypt certificates.

10. Key Takeaways

  • Containerization guarantees environment parity across development and production servers.
  • Automating SSH pipelines with GitHub Actions removes human error from deployment scripts.
  • Secure compute assets by shutting down open security group ports and delegating all routing to Nginx.
  • Configure restart rules on your docker containers to ensure high availability.
  • AWS Deploy - Auto-deployment workflow using Docker and GitHub Actions.
  • Auth Service - Dockerized Node microservice managed by this EC2 pipeline.
  • How JWT Authentication Works Under the Hood - Microservice validation layers.