Complete Guide: Deploy Next.js to AWS with Docker and CI/CD
The Architecture Overview
Our deployment setup uses several AWS services and tools working together seamlessly. GitHub Actions handles the continuous integration, automatically building your application whenever you push code. Docker containerizes your Next.js app, ensuring consistency between development and production. Amazon ECR stores your Docker images securely, while an EC2 instance serves as your actual production server.
On that server, Docker Compose orchestrates your containers, Watchtower keeps your images up-to-date automatically, Nginx acts as a reverse proxy, and Certbot manages your SSL certificates.

1Containerize Your Next.js Application
The first step is to package your Next.js application in a Docker container. A multi-stage build approach works well here — the first stage installs all dependencies and builds your application, while the second stage only includes what's needed to run it. This keeps your final image size lean and efficient.
# Stage 1: Install dependencies
FROM node:18-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Stage 2: Build the application
FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Stage 3: Production runner
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Copy only what's needed to run
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]Make sure your next.config.ts has standalone output enabled:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;2Set Up Automated CI/CD with GitHub Actions
GitHub Actions automates the entire build and deployment process whenever you push code to your main branch. The workflow authenticates with AWS using your credentials stored as GitHub secrets, builds your Docker image, tags it, and pushes it directly to your Amazon ECR repository.
name: Build and Push to ECR
on:
push:
branches: [main]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: my-nextjs-app
IMAGE_TAG: latest
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAGDon't forget to add AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as secrets in your GitHub repository settings under Settings → Secrets and variables → Actions.
3Launch an EC2 Instance and Install Docker
Create an EC2 instance on AWS to host your application. A t3.micro or t3.small instance is typically sufficient for most Next.js applications. Once running, SSH in and install Docker and Docker Compose:
# Update the system
sudo yum update -y
# Install Docker
sudo yum install -y docker
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker ec2-user
# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/\
docker-compose-$(uname -s)-$(uname -m)" \
-o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Verify installations
docker --version
docker-compose --versionLog out and back in for the group changes to take effect, then you can run Docker commands without sudo.
4Set Up Docker Compose for Orchestration
Docker Compose ties everything together on your EC2 instance. This single file defines three main services: your Next.js application pulling from ECR, Nginx serving as your reverse proxy, and Watchtower running in the background to auto-update your containers.
version: "3.8"
services:
app:
image: <your-account-id>.dkr.ecr.us-east-1.amazonaws.com/my-nextjs-app:latest
restart: always
ports:
- "3000:3000"
environment:
- NODE_ENV=production
nginx:
image: nginx:alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- app
watchtower:
image: containrrr/watchtower
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/ec2-user/.docker/config.json:/config.json
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_POLL_INTERVAL=300
command: --interval 3005Keep Your Docker Images Updated with Watchtower
Watchtower (defined in the Docker Compose above) runs inside Docker and continuously monitors your ECR repository. When it detects that a new image has been pushed by GitHub Actions, it automatically pulls the latest version and restarts your container with the new code — zero manual intervention required.
You need to authenticate Docker with ECR on the EC2 instance so Watchtower can pull images:
# Login to ECR (run this on your EC2 instance)
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
<your-account-id>.dkr.ecr.us-east-1.amazonaws.com
# The credentials are saved to ~/.docker/config.json
# which Watchtower uses to authenticate pulls6Configure Nginx as a Reverse Proxy
Nginx sits in front of your Next.js application and handles SSL/TLS termination, request forwarding, and proper header management. It listens on ports 80 and 443, redirects HTTP to HTTPS, and proxies requests to your Next.js container.
# Redirect HTTP to HTTPS
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$server_name$request_uri;
}
# HTTPS server
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL certificates (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://app: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;
}
}7Connect Your Custom Domain
Point your domain (from GoDaddy, Namecheap, or any registrar) to your EC2 instance by updating the DNS A record to your instance's public IP address. DNS propagation typically takes a few minutes to an hour.
Type: A
Name: @
Value: <your-ec2-public-ip>
TTL: 300
Type: A
Name: www
Value: <your-ec2-public-ip>
TTL: 3008Generate and Configure SSL Certificates
Certbot requests free SSL certificates from Let's Encrypt. Run it on your EC2 instance to generate certificates for your domain. Set up a cron job to automatically renew them before expiration.
# Stop Nginx temporarily (Certbot needs port 80)
docker-compose stop nginx
# Install Certbot
sudo yum install -y certbot
# Generate SSL certificate
sudo certbot certonly --standalone \
-d yourdomain.com \
-d www.yourdomain.com
# Restart Nginx with SSL
docker-compose up -d nginx
# Set up automatic certificate renewal (every month)
echo "0 0 1 * * certbot renew --quiet && \
docker-compose -f /home/ec2-user/docker-compose.yml restart nginx" \
| sudo crontab -The Complete Deployment Flow
When you're ready to deploy, you push your code to GitHub. GitHub Actions immediately kicks in, building your Docker image and pushing it to ECR. Watchtower detects the new image within minutes and pulls it, restarting your container with the updated code. Traffic comes in through your domain, Nginx handles SSL encryption and acts as a reverse proxy, and your Next.js application processes the requests.
This entire flow happens with zero downtime and requires no manual intervention — just push your code and let the automation handle the rest.
Monitoring & Maintenance
With this setup, monitoring is straightforward. Here are the key commands you'll use:
# Check running containers
docker-compose ps
# View application logs
docker-compose logs -f app
# View Nginx access logs
docker-compose logs -f nginx
# Check Watchtower activity
docker-compose logs -f watchtower
# Restart all services
docker-compose restart
# Pull latest and recreate
docker-compose pull && docker-compose up -dConclusion
You now have a production-ready deployment pipeline that automatically builds your Next.js app, deploys it to AWS with zero downtime, keeps it updated automatically, secures it with SSL, and serves it under your custom domain. Best of all, once it's set up, you only need to push code to GitHub and everything else handles itself.