Deploying AWS Stack: EC2, Docker

January 21, 2026

Last weekend I rebuilt our entire AWS infrastructure. This is what I learned about deploying full-stack applications the practical way.

What I Was Trying to deploy The stack was straightforward:

Next.js frontend Strapi CMS backend PostgreSQL database S3 for media storage Nothing fancy, but it needed to work reliably in production.

First Try: ECS Fargate I started with ECS Fargate because it seemed like the modern choice. Containerized, scalable, managed by AWS. The setup looked like this:

Internet → ALB → ECS Fargate → EFS

It worked. Services ran smoothly, auto-scaling was there if needed, and AWS handled most of the infrastructure. But after running it for a few days, I started questioning whether this was the right fit for what we actually needed.

The Rebuild: Back to EC2 I redesigned everything around a single EC2 instance. Here’s the new stack:

Internet → Nginx → Docker Compose (Strapi + PostgreSQL) → S3

Everything runs on one t3.small instance. Nginx handles SSL and routing. Docker Compose manages the services. PostgreSQL replaced SQLite. Images go to S3.

The Docker Setup

version: '3.8'
services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: strapi
      POSTGRES_USER: strapi
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
    networks:
      - app-network
  backend:
    image: your-registry.com/backend:latest
    restart: unless-stopped
    ports:
      - "1337:1337"
    environment:
      DATABASE_CLIENT: postgres
      DATABASE_HOST: postgres
      DATABASE_PORT: 5432
      DATABASE_NAME: strapi
      DATABASE_USERNAME: strapi
      DATABASE_PASSWORD: ${DB_PASSWORD}
      AWS_BUCKET: ${S3_BUCKET}
      AWS_REGION: us-east-1
    depends_on:
      - postgres
    networks:
      - app-network
  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certbot/conf:/etc/letsencrypt
    depends_on:
      - backend
    networks:
      - app-network
networks:
  app-network:

Nginx Configuration SSL with Let’s Encrypt, proxy to the backend, and some basic security headers:

events {
    worker_connections 1024;
}
http {
    upstream backend {
        server backend:1337;
    }
    server {
        listen 80;
        server_name api.yourdomain.com;
        return 301 https://$host$request_uri;
    }
    server {
        listen 443 ssl http2;
        server_name api.yourdomain.com;
        ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;
        client_max_body_size 100M;
        location / {
            proxy_pass http://backend;
            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;
        }
    }
}

Nothing complicated. Just SSL, proper headers, and routing traffic to Strapi.

The Frontend Situation For the frontend, I tried deploying the Next.js app from our monorepo to AWS Amplify. Spent an entire day fighting with build configurations. Monorepos and Amplify don’t get along well.

Eventually gave up and moved the frontend to its own repo. Deployed in an hour.

The Amplify setup is now just:

Push to main branch Amplify builds and deploys Done Database Backups Made a simple backup script that runs daily:

#!/bin/bash
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR=~/backups
mkdir -p $BACKUP_DIR
docker-compose exec -T postgres pg_dump -U strapi strapi | \
  gzip > $BACKUP_DIR/backup_${TIMESTAMP}.sql.gz
aws s3 cp $BACKUP_DIR/backup_${TIMESTAMP}.sql.gz \
  s3://backup-bucket/db/
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +7 -delete

Dumps the database, compresses it, uploads to S3, cleans up old backups. Runs via cron at 2 AM.

Deployment Process Deployment is through GitHub Actions:

name: Deploy Backend
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Login to ECR
        uses: aws-actions/amazon-ecr-login@v1
      
      - name: Build and push
        run: |
          docker build -t backend:latest .
          docker push $ECR_REGISTRY/backend:latest
      
      - name: Deploy to EC2
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            cd ~/app
            docker-compose pull
            docker-compose up -d

Push to main, image builds, EC2 pulls and restarts. Takes about 3 minutes end to end.

What I Learned SQLite in Docker is a bad idea. Every container restart wipes your data unless you’re careful with volumes. Just use PostgreSQL from the start.

ECS vs EC2 depends on your actual needs. If you’re running a single application with predictable traffic, EC2 is simpler. If you need auto-scaling and multiple services, ECS makes sense.

Monorepos are great for development, annoying for deployment. Most platforms expect a single app at the repo root. Fighting this isn’t always worth it.

SSL is easier than you think. Let’s Encrypt + Certbot handles everything. Set it up once, forget about it.

Docker Compose works fine in production. You don’t need Kubernetes or ECS for everything. Docker Compose with restart policies and health checks is reliable enough for most use cases.

When to Use What Use ECS Fargate when:

You have multiple microservices Traffic is unpredictable You want AWS to handle everything Budget isn’t tight Use EC2 when:

You have one or two services Traffic is steady You want more control You’re comfortable with basic server management Use Amplify when:

You have a standard Next.js/React app You want zero-config deployments CDN and SSL matter You’re not fighting with monorepos The Current Setup Everything runs on one EC2 instance. Nginx routes traffic. Docker Compose manages services. PostgreSQL stores data. S3 holds images. Amplify serves the frontend.

It’s simple, maintainable, and does what it needs to do. When traffic grows, I can move the database to RDS or add more EC2 instances. But right now, this works.

Resources If you’re building something similar:

Docker Compose docs AWS EC2 guide That’s it. No magic, just solid basics done right.

Comments (0)