Tutorials
This guide covers deploying your Next.js website built with Agility CMS to AWS using Amazon Elastic Container Service (ECS) with either EC2 instances or AWS Fargate. This approach provides full control over your infrastructure and supports all Next.js features including Server-Side Rendering (SSR), API routes, and Incremental Static Regeneration (ISR).
AWS ECS is a fully managed container orchestration service that allows you to run Docker containers at scale. You can deploy Next.js applications using either:
Most developers deploying Agility CMS websites to AWS ECS will be starting from an existing Next.js project:
These starters already include proper Next.js configuration, Agility CMS integration, and the necessary build scripts. You'll still need to configure your environment variables and Docker setup.
Before you begin, ensure you have:
If you don't already have an Agility CMS instance set up:
GUID (Instance ID)Live API Key (for production)Preview API Key (for development/preview)Security Key (for webhooks)You'll need these credentials for both local development and AWS ECS deployment configuration.
The recommended architecture for deploying Next.js to AWS ECS includes:
Internet
↓
Application Load Balancer (ALB)
↓
ECS Service (Fargate or EC2)
↓
Next.js Container (Port 3000)
↓
Agility CMS API
Components:
Ensure your package.json has the required scripts:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
Note: The Agility CMS Next.js starters already include proper Next.js configuration for containerized deployment.
Before deploying, ensure your Next.js application is compatible with containerized deployment:
Image Component - Use AgilityPic (Required):
⚠️ IMPORTANT: Do NOT use
next/imagefor Agility CMS images. Use theAgilityPiccomponent from@agility/nextjsinstead. AgilityPic is a simple wrapper around the standard<picture>tag that automatically handles image optimization and caching through Agility's CDN at the Edge - and it doesn't require any JavaScript!
Use AgilityPic (recommended - simplest option):
import { AgilityPic } from "@agility/nextjs";
<AgilityPic
src={fields.image.url}
alt={fields.image.label}
width={fields.image.width || 768}
height={fields.image.height || 512}
className="rounded-lg object-cover"
/>
Alternative: AgilityImage (if you need Next.js Image features):
// AgilityImage is a wrapper around next/image - use only if you need next/image features
import { AgilityImage } from "@agility/nextjs";
<AgilityImage
src={fields.image.url}
alt={fields.image.label}
width={768}
height={512}
/>
❌ Do NOT use next/image directly:
// ❌ Avoid - next/image adds unnecessary server-side processing
import Image from 'next/image';
<Image src={agilityImage.url} alt={agilityImage.label} />
Next.js Link Component:
as prop from next/link componentshref prop (Next.js 13+ removed the as prop)// ❌ Old way (Next.js 12 and earlier)
<Link href="/posts" as="/blog/posts">Posts</Link>
// ✅ Correct way (all Next.js versions)
<Link href="/posts">Posts</Link>
⚠️ CRITICAL: ALL image optimization and caching for Agility websites is handled at the Edge via Agility's CDN. You should NOT use
next/imagefor Agility CMS images. Instead, use theAgilityPiccomponent from@agility/nextjs- it's simpler and doesn't require JavaScript!
Why AgilityPic instead of next/image?
<picture> tag - no JavaScript required<picture> tagUsing AgilityPic (Recommended - Simplest Option):
import { AgilityPic } from "@agility/nextjs";
// Simple usage
<AgilityPic
src={fields.image.url}
alt={fields.image.label}
width={fields.image.width || 768}
height={fields.image.height || 512}
/>
// With styling
<AgilityPic
src={fields.image.url}
alt={fields.image.label}
width={768}
height={512}
className="rounded-lg object-cover object-center"
/>
Alternative: AgilityImage (if you need Next.js Image component features):
import { AgilityImage } from "@agility/nextjs";
// AgilityImage wraps next/image - use only if you need next/image specific features
<AgilityImage src={fields.image.url} alt={fields.image.label} width={768} height={512} />
No next.config.js image configuration needed - AgilityPic handles everything automatically through Agility's CDN.
For optimal container size and performance, enable standalone output:
// next.config.js
module.exports = {
output: 'standalone',
// ... other config
}
This creates a minimal production build with only necessary files, reducing container size and improving startup time.
Create a Dockerfile in the root of your Next.js project. Use a multi-stage build for optimal image size:
# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
# Copy package files
COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
# Install dependencies based on package manager
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Stage 2: Build the application
FROM node:20-alpine AS builder
WORKDIR /app
# Copy dependencies from deps stage
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Accept build arguments for Agility CMS (optional, can use env vars instead)
ARG AGILITY_GUID
ARG AGILITY_API_FETCH_KEY
ARG AGILITY_API_PREVIEW_KEY
ARG AGILITY_SECURITY_KEY
ARG AGILITY_LOCALES
# Set environment variables for build
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
# Build the application
RUN npm run build
# Stage 3: Production image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Create non-root user for security
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy necessary files from builder
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Start the application
CMD ["node", "server.js"]
Key points:
If you're not using standalone output, use this simpler version:
FROM node:20-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --omit=dev
# Copy application code
COPY . .
# Build the application
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start"]
Note: This simpler Dockerfile is easier to understand but produces a larger image. The multi-stage build above is recommended for production deployments.
Amazon ECR (Elastic Container Registry) stores your Docker images securely.
Authenticate Docker to ECR:
aws ecr get-login-password --region <your-region> | docker login --username AWS --password-stdin <aws_account_id>.dkr.ecr.<your-region>.amazonaws.com
Create ECR Repository:
aws ecr create-repository \
--repository-name nextjs-agility-app \
--region <your-region> \
--image-scanning-configuration scanOnPush=true \
--encryption-configuration encryptionType=AES256
Note the repository URI - You'll need this for pushing images and task definitions:
<aws_account_id>.dkr.ecr.<your-region>.amazonaws.com/nextjs-agility-app
nextjs-agility-app# Build the image locally
docker build -t nextjs-agility-app:latest .
# Test locally (optional)
docker run -p 3000:3000 \
-e AGILITY_GUID=<your-guid> \
-e AGILITY_API_FETCH_KEY=<your-fetch-key> \
-e AGILITY_API_PREVIEW_KEY=<your-preview-key> \
-e AGILITY_SECURITY_KEY=<your-security-key> \
-e AGILITY_LOCALES=en-us \
nextjs-agility-app:latest
# Tag the image
docker tag nextjs-agility-app:latest \
<aws_account_id>.dkr.ecr.<your-region>.amazonaws.com/nextjs-agility-app:latest
# Push to ECR
docker push <aws_account_id>.dkr.ecr.<your-region>.amazonaws.com/nextjs-agility-app:latest
Fargate is serverless - you don't manage EC2 instances.
nextjs-agility-clusterFor more control over infrastructure:
nextjs-agility-clustert3.medium or larger (recommended)A task definition describes your container configuration.
Go to ECS Console → Task Definitions → "Create new Task Definition"
Select launch type:
Configure task definition:
nextjs-agility-taskawsvpc (required for Fargate)0.5 vCPU (minimum) or 1 vCPU (recommended)1 GB (minimum) or 2 GB (recommended)Add container:
nextjs-app<aws_account_id>.dkr.ecr.<your-region>.amazonaws.com/nextjs-agility-app:latest3000TCPYesEnvironment variables (under "Environment"):
AGILITY_GUID=<your-guid>
AGILITY_API_FETCH_KEY=<your-fetch-key>
AGILITY_API_PREVIEW_KEY=<your-preview-key>
AGILITY_SECURITY_KEY=<your-security-key>
AGILITY_LOCALES=en-us
AGILITY_SITEMAP=website
Note: For production, consider using AWS Secrets Manager or Systems Manager Parameter Store instead of plain environment variables.
Health check (optional but recommended):
CMD-SHELL,curl -f http://localhost:3000/api/health || exit 1305603Logging (optional):
awslogs/ecs/nextjs-agility-appClick "Create"
An ALB distributes traffic across your ECS tasks and handles SSL termination.
Go to EC2 Console → Load Balancers → "Create Load Balancer"
Select "Application Load Balancer"
Configure:
nextjs-agility-albInternet-facingIPv40.0.0.0/0Configure security settings:
ELBSecurityPolicy-TLS-1-2-2017-01Configure routing:
nextjs-agility-tgIP (for Fargate) or Instance (for EC2)HTTP3000/api/health (or / if no health endpoint)nextjs-agility-tgnextjs-agility-tg (if using HTTPS)Click "Create"
Create a simple health check endpoint in your Next.js app:
// pages/api/health.js or app/api/health/route.js
export default function handler(req, res) {
res.status(200).json({ status: 'ok' });
}
// Or for App Router:
// app/api/health/route.ts
export async function GET() {
return Response.json({ status: 'ok' });
}
The ECS service maintains the desired number of running tasks.
Go to your ECS Cluster → "Services" tab → "Create"
Configure service:
Fargate or EC2 (match your task definition)nextjs-agility-tasknextjs-agility-service2 (for high availability)Rolling updateConfigure networking:
3000 from ALB security groupEnabled (if tasks need internet access)Configure load balancing:
Application Load Balancernextjs-agility-tgnextjs-app:3000Configure auto-scaling (optional):
21070%80%Click "Create"
http://<alb-dns-name>.elb.amazonaws.comhttps://<alb-dns-name>.elb.amazonaws.com (if SSL configured)If you're using EC2 launch type and want to access instances directly (useful for testing):
ec2-xx-xx-xx-xx.compute-1.amazonaws.com)http://<public-ipv4-dns>:3000 (Note: Use HTTP, not HTTPS, for direct instance access)https to httpImportant: Direct instance access bypasses the load balancer and should only be used for testing. For production, always use the ALB DNS name.
www (or @ for root domain)<alb-dns-name>.elb.amazonaws.com300Request certificate in ACM:
example.com and *.example.comUpdate ALB listener:
Automate deployments using GitHub Actions or AWS CodePipeline.
Create .github/workflows/deploy-ecs.yml:
name: Deploy to ECS
on:
push:
branches: [ main ]
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: nextjs-agility-app
ECS_SERVICE: nextjs-agility-service
ECS_CLUSTER: nextjs-agility-cluster
ECS_TASK_DEFINITION: nextjs-agility-task
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image to Amazon ECR
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
- name: Download task definition
run: |
aws ecs describe-task-definition \
--task-definition ${{ env.ECS_TASK_DEFINITION }} \
--query taskDefinition > task-definition.json
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: nextjs-app
image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
Here's a complete reference for Agility CMS environment variables needed in ECS:
AGILITY_GUID=xxx
Your Agility CMS instance GUID. Found in Settings > API Keys in Agility CMS.
AGILITY_API_FETCH_KEY=xxx
Your Live API Key for fetching published content. Found in Settings > API Keys in Agility CMS.
AGILITY_API_PREVIEW_KEY=xxx
Your Preview API Key for fetching draft content. Found in Settings > API Keys in Agility CMS.
AGILITY_SECURITY_KEY=xxx
Your Security Key for webhook authentication. Found in Settings > API Keys in Agility CMS.
AGILITY_LOCALES=en-us
Comma-separated list of locale codes (without spaces). Examples:
en-usen-us,fr-ca,es-esAGILITY_SITEMAP=website
The sitemap reference name if you have multiple sitemaps. Defaults to website if not specified.
For production, consider storing sensitive values in:
AWS Secrets Manager:
aws secretsmanager create-secret \
--name agility-cms-credentials \
--secret-string '{"GUID":"xxx","API_FETCH_KEY":"xxx","API_PREVIEW_KEY":"xxx","SECURITY_KEY":"xxx"}'
Then reference in task definition:
{
"secrets": [
{
"name": "AGILITY_GUID",
"valueFrom": "arn:aws:secretsmanager:region:account:secret:agility-cms-credentials:GUID::"
}
]
}
Systems Manager Parameter Store:
aws ssm put-parameter \
--name /agility/guid \
--value "xxx" \
--type "SecureString"
Issue: Container fails to start
Solution:
Issue: Health checks failing
Solution:
/api/health)0.0.0.0:3000 (not localhost:3000)Issue: Cannot pull image from ECR
Solution:
AmazonEC2ContainerRegistryReadOnly policyIssue: Cannot access application via ALB
Solution:
Issue: Application cannot reach Agility CMS API
Solution:
Issue: Slow response times
Solution:
Issue: High costs
Solution:
AWS ECS does not natively support preview deployments like some other platforms. However, you can set up preview environments:
nextjs-agility-service-previewAGILITY_API_PREVIEW_KEY)For staging/preview environments, you can run a container in development mode that fetches staging content from Agility CMS:
Create a separate task definition with:
AGILITY_API_PREVIEW_KEY (for staging content)["npm", "run", "dev"] instead of ["npm", "run", "start"]Deploy to a separate ECS service or run as a one-off task
Access via ALB or directly via EC2 instance (for testing)
Note: Development mode containers are useful for previewing draft content but consume more resources. Scale them down when not in use.
Use AWS Amplify for preview deployments (which supports PR previews) and ECS for production. This provides the best of both worlds - managed previews with Amplify and full control with ECS for production.
# Build and push Docker image
docker build -t nextjs-agility-app:latest .
docker tag nextjs-agility-app:latest <ecr-uri>:latest
docker push <ecr-uri>:latest
# Update ECS service (forces new deployment)
aws ecs update-service \
--cluster nextjs-agility-cluster \
--service nextjs-agility-service \
--force-new-deployment
# View running tasks
aws ecs list-tasks --cluster nextjs-agility-cluster
# View logs
aws logs tail /ecs/nextjs-agility-app --follow
This guide covers deploying Next.js applications with Agility CMS to AWS ECS. For platform-specific questions, refer to the AWS ECS documentation or Agility CMS support.