DEV Community

Cover image for Deploy Next.js Application on AWS Lightsail with GitHub Actions
Ali Sahin
Ali Sahin

Posted on

Deploy Next.js Application on AWS Lightsail with GitHub Actions

In this article, we'll walk through deploying Next.js and Node.js applications on AWS Lightsail using Nginx, PM2, GitHub Actions, and Cloudflare.

Prerequisites

Before starting, ensure you have:

  • AWS account with Lightsail access
  • GitHub account for repository hosting
  • Cloudflare account for DNS management
  • Domain name pointed to Cloudflare nameservers
  • Basic knowledge of Linux commands and Git
  • Node.js applications ready for deployment

Architecture Overview

Our deployment architecture includes:

  • AWS Lightsail: Virtual private server hosting the applications
  • Nginx: Reverse proxy routing traffic to applications
  • PM2: Process manager keeping Node.js apps running
  • GitHub Actions: CI/CD pipeline for automated deployments
  • Cloudflare: DNS management and SSL/TLS termination

Performance Improvement

This architecture provides several performance benefits:

Application-Level Performance:
Nginx serves as an efficient reverse proxy, handling SSL termination and static file serving, which reduces load on the Node.js applications. PM2 enables zero-downtime deployments through its cluster mode and automatic restarts, ensuring applications remain available during updates.

Caching and CDN:
Cloudflare's global CDN caches static assets at edge locations worldwide, significantly reducing latency for users. Nginx can also implement additional caching layers for API responses and dynamic content, further improving response times.

Resource Optimization:
PM2's cluster mode allows Node.js applications to utilize all available CPU cores, maximizing server resource utilization. Lightsail provides predictable performance with fixed resource allocations, making capacity planning straightforward.

Implementation Steps

Step 1: Set Up AWS Lightsail Instance

Create a new Lightsail instance:

# Use AWS Console or CLI
aws lightsail create-instances \
  --instance-names my-app-server \
  --availability-zone us-east-1a \
  --blueprint-id ubuntu_22_04 \
  --bundle-id nano_2_0
Enter fullscreen mode Exit fullscreen mode

Console Steps:

  1. Log in to AWS Lightsail console
  2. Click "Create instance"
  3. Select Linux/Unix platform
  4. Choose Ubuntu 22.04 LTS blueprint
  5. Select an instance plan (recommend at least $5/month for production)
  6. Name your instance (e.g., "my-app-server")
  7. Click "Create instance"

Once the instance is running, note its static IP address. Configure the Lightsail firewall:

Firewall Rules:

  • SSH (Port 22) - Your IP only
  • HTTP (Port 80) - All traffic
  • HTTPS (Port 443) - All traffic

Step 2: Connect and Set Up the Server

Connect to your Lightsail instance via SSH:

# Download the SSH key from Lightsail console
chmod 400 lightsail-key.pem
ssh -i lightsail-key.pem ubuntu@YOUR_STATIC_IP
Enter fullscreen mode Exit fullscreen mode

Update the system and install required packages:

# Update package list
sudo apt update && sudo apt upgrade -y

# Install Nginx and Git
sudo apt install -y nginx git

# Install Node.js 24.x (LTS)
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs

# Verify installation
node --version
npm --version

# Install PM2 globally
sudo npm install -g pm2

# Verify PM2 installation
pm2 --version
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure Nginx as Reverse Proxy

Create Nginx configuration for your applications:

# Create configuration file
sudo nano /etc/nginx/sites-available/myapp
Enter fullscreen mode Exit fullscreen mode

Add the following configuration:


server
{
    listen 80;
    server_name yourdomain.com;

    location /api
    {
        proxy_pass http://localhost:4000; # backend port
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;

    # CORS headers (if needed)
    add_header Access-Control-Allow-Origin *;

    # Rate limiting for API endpoints
    location /api {
        limit_req zone=api_limit burst=20 nodelay;
        proxy_pass http://localhost:4000;
        proxy_set_header Host $host;
    }

    }

    location /
    {
        proxy_pass http://localhost:3000; # frontend port
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection '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;

    # Cache static assets
    location /_next/static {
      proxy_pass http://localhost:3000;
      proxy_cache_valid 60m;
      add_header Cache-Control "public, max-age=3600, immutable";
    }
    }
}

# Rate limiting configuration
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
Enter fullscreen mode Exit fullscreen mode

Enable the configuration:

# Create symbolic link
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/

# Remove default configuration
sudo rm /etc/nginx/sites-enabled/default

# Test configuration
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

# Enable Nginx to start on boot
sudo systemctl enable nginx
Enter fullscreen mode Exit fullscreen mode

Step 4: Set Up Application Directories

Create directories for your applications:

# Create application directories
sudo mkdir -p /var/www/my-app

# Set ownership to ubuntu user
sudo chown -R ubuntu:ubuntu /var/www/my-app

# Create environment files directory
mkdir -p /home/ubuntu/.env-files
Enter fullscreen mode Exit fullscreen mode

Step 5: Configure PM2 for Process Management

Create PM2 ecosystem configuration file:

# Create ecosystem file
nano /var/www/ecosystem.config.js
Enter fullscreen mode Exit fullscreen mode

Add the following PM2 configuration:

module.exports = {
  apps: [
    {
      name: "frontend",
      cwd: "/var/www/my-app/frontend",
      script: "npm",
      args: "start",
      env: {
        NODE_ENV: "production",
        PORT: 3000,
      },
      instances: 2,
      exec_mode: "cluster",
      autorestart: true,
      watch: false,
      max_memory_restart: "500M",
      error_file: "/var/www/logs/nextjs-error.log",
      out_file: "/var/www/logs/nextjs-out.log",
      merge_logs: true,
      time: true,
    },
    {
      name: "api",
      cwd: "/var/www/my-app/api",
      script: "dist/index.js", // or 'src/index.js' if not using TypeScript
      env: {
        NODE_ENV: "production",
        PORT: 4000,
      },
      instances: 2,
      exec_mode: "cluster",
      autorestart: true,
      watch: false,
      max_memory_restart: "500M",
      error_file: "/var/www/logs/api-error.log",
      out_file: "/var/www/logs/api-out.log",
      merge_logs: true,
      time: true,
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

Create logs directory:

sudo mkdir -p /var/www/logs
sudo chown -R ubuntu:ubuntu /var/www/logs
Enter fullscreen mode Exit fullscreen mode

Step 6: Configure Cloudflare DNS and SSL

DNS Configuration:

  1. Log in to your Cloudflare dashboard
  2. Select your domain
  3. Go to DNS settings
  4. Add A records:
    • Name: nextapp, Content: YOUR_LIGHTSAIL_STATIC_IP, Proxy: Enabled (Orange cloud)
    • Name: api, Content: YOUR_LIGHTSAIL_STATIC_IP, Proxy: Enabled (Orange cloud)

SSL/TLS Configuration:

  1. Go to SSL/TLS settings in Cloudflare
  2. Set SSL/TLS encryption mode to "Flexible" (or "Full" if you configure SSL on Lightsail)
  3. Enable "Always Use HTTPS"
  4. Enable "Automatic HTTPS Rewrites"
  5. For production, consider "Full (Strict)" mode with origin certificates

Optional: Generate Cloudflare Origin Certificate (for Full Strict mode):

  1. In Cloudflare, go to SSL/TLS → Origin Server
  2. Click "Create Certificate"
  3. Keep default settings (RSA, 15-year validity)
  4. Save the certificate and private key

Install on Lightsail:

# Save certificate
sudo nano /etc/ssl/certs/cloudflare-origin.pem
# Paste certificate content

# Save private key
sudo nano /etc/ssl/private/cloudflare-origin.key
# Paste private key content

# Set permissions
sudo chmod 644 /etc/ssl/certs/cloudflare-origin.pem
sudo chmod 600 /etc/ssl/private/cloudflare-origin.key
Enter fullscreen mode Exit fullscreen mode

Update Nginx configuration to use SSL:

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    ssl_certificate /etc/ssl/certs/cloudflare-origin.pem;
    ssl_certificate_key /etc/ssl/private/cloudflare-origin.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection '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;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$server_name$request_uri;
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Set Up GitHub Actions SSH Access

Generate SSH key for GitHub Actions:

# On your Lightsail instance, create deployment user SSH key
ssh-keygen -t ed25519 -C "myapp-github-actions" -f ~/.ssh/myapp-github-actions -N ""

# Add public key to authorized_keys
cat ~/.ssh/myapp-github-actions.pub >> ~/.ssh/authorized_keys

# Display private key (copy this for GitHub Secrets)
cat ~/.ssh/myapp-github-actions
Enter fullscreen mode Exit fullscreen mode

Add Secrets to GitHub Repository:

  1. Go to your GitHub repository
  2. Navigate to Settings → Secrets and variables → Actions
  3. Add the following secrets:
    • LIGHTSAIL_HOST: Your Lightsail static IP
    • LIGHTSAIL_USERNAME: ubuntu
    • LIGHTSAIL_SSH_KEY: Contents of the private key from above
    • LIGHTSAIL_PORT: 22

Step 8: Create GitHub Actions Workflow for monorepo Application

Create .github/workflows/deploy-to-lightsail.yml:

name: Deploy To Lightsail

on:
  push:
    branches:
      - main

jobs:
  build-deploy:
    name: Build & Deploy
    runs-on: ubuntu-latest
    continue-on-error: false
    outputs:
      node_modules_cache_key: ${{ steps.cache.outputs.cache-hit }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Use Node.js 24
        uses: actions/setup-node@v4
        with:
          node-version: 24

      - name: Cache node_modules
        id: cache
        uses: actions/cache@v4
        with:
          path: |
            node_modules
            ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}

      # ---------------- Backend Build ----------------
      - name: Install backend dependencies
        working-directory: api
        run: npm ci

      - name: Build backend
        working-directory: api
        run: npm run build

      # ---------------- Frontend Build ----------------
      - name: Install frontend dependencies
        working-directory: frontend
        run: npm ci

      - name: Build frontend
        working-directory: frontend
        run: npm run build

      # ---------------- Package Artifacts ----------------
      - name: Prepare deployment package
        run: |
          mkdir -p deploy/api deploy/frontend
          cp -r api/dist deploy/api/
          cp api/package*.json deploy/api/
          cp -r frontend/.next frontend/public frontend/package*.json deploy/frontend/
          cp ecosystem.config.js deploy/

      # ---------------- Deploy to Server ----------------
      - name: Deploy via rsync
        uses: burnett01/rsync-deployments@7.0.1
        with:
          switches: -avzr --delete
          path: deploy/
          remote_path: /var/www/my-app
          remote_host: ${{ secrets.SERVER_IP }}
          remote_user: ${{ secrets.SERVER_USER }}
          remote_key: ${{ secrets.SSH_PRIVATE_KEY }}

      # ---------------- Install & Restart on Server ----------------
      - name: SSH and restart apps
        uses: appleboy/ssh-action@v1.2.2
        with:
          host: ${{ secrets.SERVER_IP }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cp ~/.env-files/.env.api /var/www/my-app/api/.env            
            cp ~/.env-files/.env.frontend /var/www/my-app/frontend/.env.production
            cd /var/www/my-app/api
            npm ci
            cd /var/www/my-app/frontend
            npm ci --omit=dev
            cd /var/www/my-app
            pm2 startOrRestart ecosystem.config.js
Enter fullscreen mode Exit fullscreen mode

Step 9: Test GitHub Actions Deployment

Push changes to your main branch to trigger the workflow:

git add .
git commit -m "Set up GitHub Actions deployment"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Monitor the Actions tab in your GitHub repository to ensure the deployment completes successfully.

Step 10: Initial Manual Deployment

Before using GitHub Actions, manually deploy your applications:

# Clone your repository
cd /var/www/my-app
git clone https://github.com/yourusername/my-app.git .

# Build Next.js app
cd /var/www/my-app/frontend
npm install
npm run build

# Build Node.js API
cd /var/www/my-app/api
npm install
npm run build  # If using TypeScript

# Start applications with PM2
pm2 start /var/www/my-app/ecosystem.config.js

# Save PM2 configuration
pm2 save

# Set PM2 to start on system boot
pm2 startup
# Follow the instructions provided by the command
Enter fullscreen mode Exit fullscreen mode

Step 11: Configure Environment Variables

Store environment variables securely:

# Create .env file for Next.js app
nano /var/www/my-app/frontend/.env.production

# Add your variables
NEXT_PUBLIC_API_URL=https://yourdomain.com/api
DATABASE_URL=your_database_url
API_SECRET_KEY=your_secret_key

# Create .env file for Node.js API
nano /var/www/my-app/api/.env

# Add your variables
PORT=4000
DATABASE_URL=your_database_url
JWT_SECRET=your_jwt_secret
NODE_ENV=production

# Secure the files
chmod 600 /var/www/my-app/frontend/.env.production
chmod 600 /var/www/my-app/api/.env
Enter fullscreen mode Exit fullscreen mode

Add environment variables to GitHub Secrets for use in workflows if needed.

Verification

After deployment, verify everything is working:

# Check PM2 status
pm2 status

# View application logs
pm2 logs nextjs-app --lines 50
pm2 logs nodejs-api --lines 50

# Check Nginx status
sudo systemctl status nginx

# Test local endpoints
curl http://localhost:3000
curl http://localhost:4000

# Test public endpoints
curl https://yourdomain.com
curl https://yourdomain.com/api
Enter fullscreen mode Exit fullscreen mode

Security Enhancement

SSL/TLS Encryption:
Cloudflare provides free SSL certificates with automatic renewal, ensuring all traffic between users and your applications is encrypted. Cloudflare's Full (Strict) SSL mode encrypts traffic end-to-end, from users to Cloudflare to your Lightsail instance.

DDoS Protection:
Cloudflare's built-in DDoS protection shields your applications from attacks at the edge, preventing malicious traffic from reaching your Lightsail instance. This protection is included in the free tier and automatically scales with attack volume.

Firewall and Access Control:
Lightsail's firewall allows you to restrict access to specific ports, ensuring only necessary services are exposed. Cloudflare's WAF (Web Application Firewall) can be configured to block common attack patterns and malicious requests before they reach your server.

Secure Deployments:
GitHub Actions with OIDC or SSH key authentication eliminates the need for long-lived credentials in your CI/CD pipeline. PM2 runs applications with limited user privileges, following the principle of least privilege.

Cost Efficiency

Predictable Pricing:
AWS Lightsail offers fixed monthly pricing starting at $3.50/month, including data transfer allowances. This predictable cost structure makes budgeting simple compared to pay-per-use EC2 instances.

Cloudflare Free Tier:
Cloudflare's free tier includes DNS management, SSL certificates, and DDoS protection at no cost. The CDN caching reduces bandwidth usage on your Lightsail instance, potentially allowing you to use a smaller instance size.

Resource Consolidation:
Hosting multiple applications on a single Lightsail instance with Nginx as a reverse proxy maximizes resource utilization. PM2's efficient process management ensures applications use only necessary resources, preventing waste.

No Additional Service Costs:
Unlike managed container services or serverless platforms, Lightsail with Nginx and PM2 has no additional service fees—you only pay for the compute instance.

Troubleshooting

Issue: PM2 Applications Not Starting

Symptoms: PM2 shows applications as "errored" or "stopped"

Solution:

# Check detailed logs
pm2 logs nextjs-app --lines 100

# Check if port is already in use
sudo netstat -tulpn | grep :3000

# Verify Node.js version
node --version

# Restart with verbose output
pm2 delete nextjs-app
pm2 start /var/www/ecosystem.config.js --only nextjs-app
Enter fullscreen mode Exit fullscreen mode

Issue: Nginx 502 Bad Gateway

Symptoms: Nginx returns 502 error when accessing applications

Solution:

# Check if applications are running
pm2 status

# Verify applications are listening on correct ports
curl http://localhost:3000
curl http://localhost:4000

# Check Nginx error logs
sudo tail -f /var/log/nginx/error.log

# Test Nginx configuration
sudo nginx -t

# Restart Nginx
sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

Issue: GitHub Actions Deployment Fails

Symptoms: GitHub Actions workflow shows SSH connection errors

Solution:

  1. Verify SSH key is correctly added to GitHub Secrets
  2. Check Lightsail firewall allows SSH from GitHub Actions IPs
  3. Test SSH connection manually:
ssh -i myapp-github-actions ubuntu@YOUR_LIGHTSAIL_IP
Enter fullscreen mode Exit fullscreen mode
  1. Ensure authorized_keys file has correct permissions:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

Issue: Cloudflare SSL Errors

Symptoms: "Too many redirects" or "SSL handshake failed"

Solution:

  1. Check Cloudflare SSL mode matches your Nginx configuration:
    • Flexible: No SSL on Lightsail (HTTP only)
    • Full: Self-signed certificate on Lightsail
    • Full (Strict): Cloudflare Origin Certificate on Lightsail
  2. Disable "Always Use HTTPS" temporarily in Cloudflare
  3. Check Nginx SSL certificate paths are correct
  4. Verify Nginx is listening on port 443:
sudo netstat -tulpn | grep :443
Enter fullscreen mode Exit fullscreen mode

Issue: High Memory Usage

Symptoms: PM2 apps restarting frequently due to memory limits

Solution:

# Check memory usage
pm2 status
free -h

# Adjust max_memory_restart in ecosystem.config.js
# Reduce number of instances if needed
# Consider upgrading Lightsail plan

# Monitor memory over time
pm2 monit
Enter fullscreen mode Exit fullscreen mode

Monitoring and Maintenance

PM2 Monitoring:

# View real-time monitoring
pm2 monit

# View process list
pm2 status

# View logs
pm2 logs

# Flush logs
pm2 flush
Enter fullscreen mode Exit fullscreen mode

Nginx Monitoring:

# Check access logs
sudo tail -f /var/log/nginx/access.log

# Check error logs
sudo tail -f /var/log/nginx/error.log

# Check Nginx status
sudo systemctl status nginx
Enter fullscreen mode Exit fullscreen mode

System Monitoring:

# Check disk space
df -h

# Check memory usage
free -h

# Check CPU usage
top

# Check running processes
ps aux
Enter fullscreen mode Exit fullscreen mode

Regular Maintenance Tasks:

  1. Update system packages monthly:
sudo apt update && sudo apt upgrade -y
Enter fullscreen mode Exit fullscreen mode
  1. Rotate logs to prevent disk space issues:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7
Enter fullscreen mode Exit fullscreen mode
  1. Monitor Lightsail metrics in AWS console
  2. Review Cloudflare analytics for traffic patterns
  3. Set up CloudWatch alarms for Lightsail instance health

Next Steps

After successfully deploying your applications, consider these enhancements:

Caching Layer:

  • Install Redis for session storage and caching
  • Configure Nginx caching for static assets
  • Use Cloudflare's cache rules for optimal CDN performance

Monitoring and Logging:

  • Set up PM2 Plus for advanced monitoring
  • Configure CloudWatch Logs for centralized logging
  • Set up Uptime monitoring with services like UptimeRobot

Backup Strategy:

  • Configure Lightsail automatic snapshots
  • Set up database backup scripts
  • Store critical data in S3

Scaling Considerations:

  • Use Lightsail load balancer with multiple instances for high availability
  • Migrate to ECS or EC2 Auto Scaling Groups for advanced scaling needs
  • Consider using Amazon CloudFront in addition to Cloudflare for AWS-native CDN

Security Hardening:

  • Configure Cloudflare WAF rules for application protection
  • Implement rate limiting at multiple layers
  • Regular security updates and vulnerability scanning

Summary

In this tutorial, we successfully deployed Next.js and Node.js applications on AWS Lightsail using a production-ready stack. By combining Nginx as a reverse proxy, PM2 for robust process management, GitHub Actions for automated CI/CD, and Cloudflare for DNS and SSL management, we created a reliable, secure, and cost-effective deployment pipeline.

Top comments (0)