How to Deploy Docker to AWS EC2 Step by Step

How to Deploy Docker to AWS EC2 Step by Step

Profile-Image
Bright SEO Tools in saas Published: Apr 04, 2026 | Updated: Apr 04, 2026 · 2 months ago
0:00

How to Deploy Docker to AWS EC2 Step by Step

Deploying Docker containers to AWS EC2 gives you full control over your infrastructure while avoiding the complexity and cost of managed container services like ECS or EKS. For small to medium applications, a single EC2 instance running Docker Compose provides a reliable production environment at a fraction of the cost—often $20-50/month compared to $100+ for managed alternatives. The tradeoff is manual infrastructure management, but for teams comfortable with Linux administration, this tradeoff is worth the savings and simplicity.

This guide walks through the complete deployment process: launching an EC2 instance, configuring security groups, installing Docker, deploying a containerized application with Docker Compose, setting up SSL with Let's Encrypt, implementing automated deployments, and configuring monitoring. You'll have a production-ready deployment by the end, with practical configurations tested on real applications.

We'll cover instance setup, Docker installation, application deployment, reverse proxy configuration, SSL setup, and CI/CD integration.

Prerequisites and Planning

Before launching infrastructure, clarify requirements. How much traffic will your application handle? Do you need persistent storage? What level of availability do you require? These questions determine instance type, storage configuration, and backup strategy.

For most web applications serving moderate traffic (thousands of daily users), a t3.small instance (2 vCPU, 2 GB RAM) works well. Database-heavy applications might need t3.medium (4 GB RAM). High-traffic applications or those with intensive processing benefit from c5 or m5 instances. Start conservative—you can resize EC2 instances with brief downtime if needed.

AWS Account Setup

You need an AWS account with IAM user credentials. Avoid using root account credentials. Create an IAM user with programmatic access and attach the AmazonEC2FullAccess policy. Download the access key ID and secret access key—you'll use these for AWS CLI commands and GitHub Actions later.

Install AWS CLI locally to manage infrastructure:

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

aws configure

Enter your access key, secret key, default region (like us-east-1), and output format (json).

Launching an EC2 Instance

Launch an Ubuntu 22.04 LTS instance through the AWS Console or CLI. Ubuntu is well-documented, widely supported, and includes recent package versions. Amazon Linux is an alternative with tighter AWS integration, but Ubuntu's broader community makes troubleshooting easier.

Using AWS Console

Navigate to EC2 Dashboard and click Launch Instance. Configure:

  • Name: Something descriptive like production-app-server
  • AMI: Ubuntu Server 22.04 LTS (HVM), SSD Volume Type
  • Instance type: t3.small for starters
  • Key pair: Create a new key pair or select an existing one. Download the .pem file—you'll need it for SSH access
  • Network settings: Allow SSH (port 22), HTTP (port 80), and HTTPS (port 443) from anywhere (0.0.0.0/0)
  • Storage: 20 GB gp3 SSD minimum. 30-50 GB provides comfortable room for Docker images and logs

Click Launch Instance. AWS creates the instance and assigns a public IP address.

Using AWS CLI

For reproducible deployments, use CLI commands. First, create a security group:

aws ec2 create-security-group \
  --group-name docker-app-sg \
  --description "Security group for Docker application"

aws ec2 authorize-security-group-ingress \
  --group-name docker-app-sg \
  --protocol tcp --port 22 --cidr 0.0.0.0/0

aws ec2 authorize-security-group-ingress \
  --group-name docker-app-sg \
  --protocol tcp --port 80 --cidr 0.0.0.0/0

aws ec2 authorize-security-group-ingress \
  --group-name docker-app-sg \
  --protocol tcp --port 443 --cidr 0.0.0.0/0

Launch the instance:

aws ec2 run-instances \
  --image-id ami-0c7217cdde317cfec \
  --instance-type t3.small \
  --key-name your-key-pair-name \
  --security-groups docker-app-sg \
  --block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":30,"VolumeType":"gp3"}}]' \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=production-app-server}]'

Note the AMI ID varies by region. Find the current Ubuntu 22.04 AMI for your region at Ubuntu Cloud Images.

Warning: Opening SSH (port 22) to all IP addresses (0.0.0.0/0) is a security risk. For production systems, restrict SSH to known IP addresses or use AWS Systems Manager Session Manager for access without exposing SSH publicly.

Connecting to Your EC2 Instance

Once the instance is running, note its public IP address from the EC2 console. Connect via SSH:

chmod 400 your-key-pair.pem
ssh -i your-key-pair.pem ubuntu@your-instance-public-ip

If you see "Permission denied" errors, verify the username is ubuntu (for Ubuntu AMIs) or ec2-user (for Amazon Linux), and that the key file has correct permissions (400).

First login, update the system:

sudo apt update
sudo apt upgrade -y

This updates package lists and installs security patches.

Installing Docker on EC2

Install Docker using the official Docker repository for the latest version. Ubuntu's default repositories often have outdated Docker versions.

Docker Engine Installation

sudo apt install -y ca-certificates curl gnupg lsb-release

sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Verify installation:

docker --version
docker compose version

Add your user to the docker group to run Docker commands without sudo:

sudo usermod -aG docker ubuntu
newgrp docker

Test with:

docker run hello-world

This downloads a test image and runs a container, confirming Docker works correctly.

Deploying Your Application with Docker Compose

Transfer your application code to the EC2 instance. You can use git clone if your code is in a repository, or scp to copy files directly.

Using Git for Deployment

Install git if not already available:

sudo apt install -y git

Clone your repository:

git clone https://github.com/yourusername/your-app.git
cd your-app

For private repositories, set up SSH keys or use deploy tokens. Generate an SSH key on the EC2 instance and add it to your GitHub account as a deploy key:

ssh-keygen -t ed25519 -C "deploy@production"
cat ~/.ssh/id_ed25519.pub

Copy the output and add it to your repository's Settings → Deploy keys.

Sample Docker Compose Configuration

A typical production docker-compose.yml for a Node.js application with PostgreSQL:

version: '3.8'

services:
  app:
    image: your-dockerhub-username/your-app:latest
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://postgres:${DB_PASSWORD}@db:5432/myapp
      SESSION_SECRET: ${SESSION_SECRET}
    depends_on:
      - db
    volumes:
      - app-logs:/app/logs

  db:
    image: postgres:15-alpine
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: myapp
    volumes:
      - postgres-data:/var/lib/postgresql/data
    ports:
      - "127.0.0.1:5432:5432"

volumes:
  postgres-data:
  app-logs:

Note the database port mapping (127.0.0.1:5432:5432) restricts external access—only processes on the EC2 instance can connect.

Environment Variables Configuration

Create a .env file with production secrets:

DB_PASSWORD=your_secure_database_password
SESSION_SECRET=your_secure_session_secret

Generate secure random strings for production:

openssl rand -base64 32

Set restrictive permissions on the .env file:

chmod 600 .env

Starting the Application

Pull images and start containers:

docker compose pull
docker compose up -d

The -d flag runs containers in detached mode (background). Check status:

docker compose ps
docker compose logs -f app

Your application should now be running on port 3000. Test locally:

curl http://localhost:3000

You should see your application's response.

Setting Up Nginx as Reverse Proxy

Running your application directly on port 80 requires root privileges and doesn't support SSL termination or multiple applications. Nginx acts as a reverse proxy, forwarding requests to your Docker containers while handling SSL, compression, and static file serving.

Installing Nginx

sudo apt install -y nginx

Create an Nginx configuration for your application:

sudo nano /etc/nginx/sites-available/myapp

Add this configuration:

server {
    listen 80;
    server_name your-domain.com www.your-domain.com;

    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;
    }
}

Enable the configuration:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

The nginx -t command tests configuration syntax before applying changes.

Configuring DNS

Point your domain to the EC2 instance's public IP. In your DNS provider (Cloudflare, Route53, Namecheap, etc.), create an A record:

  • Type: A
  • Name: @ (for root domain) or subdomain name
  • Value: Your EC2 instance public IP
  • TTL: 300 (5 minutes)

DNS propagation takes a few minutes to hours depending on provider and TTL. Test with:

nslookup your-domain.com

Once DNS resolves to your EC2 IP, visiting http://your-domain.com should show your application.

Pro Tip: Consider using AWS Route 53 for DNS if you're already on AWS. It integrates seamlessly with EC2 and supports features like health checks and automatic failover. The cost is minimal ($0.50/month per hosted zone plus $0.40 per million queries).

Setting Up SSL with Let's Encrypt

Let's Encrypt provides free SSL certificates with automatic renewal. Certbot automates the process of obtaining and installing certificates.

Installing Certbot

sudo apt install -y certbot python3-certbot-nginx

Obtain and install a certificate:

sudo certbot --nginx -d your-domain.com -d www.your-domain.com

Certbot asks for your email (for renewal notifications) and agreement to terms of service. It automatically modifies your Nginx configuration to enable HTTPS and redirect HTTP to HTTPS.

Test automatic renewal:

sudo certbot renew --dry-run

If this succeeds, certificates will renew automatically before expiration. Certbot installs a cron job or systemd timer that checks for renewal twice daily.

Verifying HTTPS

Visit https://your-domain.com. You should see a secure connection with a valid certificate. Test SSL configuration at SSL Labs for security grade and potential issues.

Implementing Automated Deployments

Manual SSH deployments work but don't scale. Automate deployments with GitHub Actions that push new images and restart containers when you push to main branch.

Setting Up GitHub Actions

Create .github/workflows/deploy.yml in your repository:

name: Deploy to EC2

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

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

    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: your-dockerhub-username/your-app:latest

    - name: Deploy to EC2
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.EC2_HOST }}
        username: ubuntu
        key: ${{ secrets.EC2_SSH_KEY }}
        script: |
          cd /home/ubuntu/your-app
          git pull origin main
          docker compose pull
          docker compose up -d
          docker image prune -f

Configure GitHub secrets (Settings → Secrets and variables → Actions):

  • DOCKER_USERNAME: Your Docker Hub username
  • DOCKER_PASSWORD: Docker Hub access token (generate from Docker Hub account settings)
  • EC2_HOST: Your EC2 instance public IP or domain
  • EC2_SSH_KEY: Contents of your .pem private key file

Now when you push to main branch, GitHub Actions builds a new image, pushes it to Docker Hub, SSHs into your EC2 instance, pulls the new image, and restarts containers.

Zero-Downtime Deployments

The basic deployment causes brief downtime while containers restart. For zero-downtime deployments, use blue-green approach with two compose files or implement rolling updates through orchestration tools. For simpler applications, the few-second downtime is acceptable.

Monitoring and Logging

Production systems need monitoring to detect failures and performance degradation. Start with basic monitoring and expand as needed.

CloudWatch Metrics

EC2 instances automatically send metrics to CloudWatch (CPU, network, disk). Enable detailed monitoring for 1-minute resolution (costs extra). Install CloudWatch agent for memory and disk metrics:

wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
sudo dpkg -i amazon-cloudwatch-agent.deb

sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-config-wizard

The wizard guides you through configuration. Select metrics you want to collect (memory, disk, processes).

Application Logging

Docker logs are accessible via docker compose logs, but they're lost when containers are removed. Configure a logging driver to persist logs:

services:
  app:
    image: your-app:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

This limits log files to 10 MB each, keeping the last 3 files (30 MB total). For centralized logging, use CloudWatch Logs driver:

services:
  app:
    image: your-app:latest
    logging:
      driver: awslogs
      options:
        awslogs-group: /ecs/myapp
        awslogs-region: us-east-1
        awslogs-stream-prefix: app

This requires creating a CloudWatch log group and configuring EC2 instance IAM role with CloudWatch Logs permissions.

Uptime Monitoring

Use external monitoring services to check if your application is accessible. Free options include UptimeRobot, Better Uptime, or AWS Route 53 health checks. These ping your application periodically and alert you via email or SMS if it's down.

Database Backups

Regular backups prevent data loss. Automate PostgreSQL backups with a cron job:

sudo crontab -e

Add a daily backup at 2 AM:

0 2 * * * docker exec your-app-db-1 pg_dump -U postgres myapp | gzip > /home/ubuntu/backups/myapp-$(date +\%Y\%m\%d-\%H\%M\%S).sql.gz

Create the backups directory:

mkdir -p /home/ubuntu/backups

This creates compressed SQL dumps. For production, upload backups to S3 for durability:

0 2 * * * docker exec your-app-db-1 pg_dump -U postgres myapp | gzip | aws s3 cp - s3://your-backup-bucket/myapp-$(date +\%Y\%m\%d-\%H\%M\%S).sql.gz

Install AWS CLI on the instance and configure it with IAM role or credentials that have S3 write permissions.

Security Hardening

Firewall Configuration with UFW

Ubuntu includes UFW (Uncomplicated Firewall) which simplifies iptables management:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable

This blocks all incoming traffic except SSH, HTTP, and HTTPS.

Automatic Security Updates

Enable unattended upgrades for automatic security patches:

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Select "Yes" to enable automatic updates. This installs security updates automatically, reducing exposure to known vulnerabilities.

SSH Hardening

Disable password authentication (require key-based auth only):

sudo nano /etc/ssh/sshd_config

Set:

PasswordAuthentication no
PermitRootLogin no

Restart SSH:

sudo systemctl restart ssh

This prevents brute-force password attacks and blocks root login.

Security Note: Before disabling password authentication, ensure you have working key-based SSH access. If you lock yourself out, you'll need to use AWS Systems Manager Session Manager or the EC2 instance connect feature to regain access.

Cost Optimization

Reserved Instances

If you're running this EC2 instance long-term, purchase a Reserved Instance for significant savings. A 1-year commitment reduces costs by ~40%, 3-year by ~60%. Calculate breakeven point—Reserved Instances make sense if you'll run continuously for at least 8-10 months.

Spot Instances for Non-Critical Workloads

Spot Instances offer up to 90% discount but can be interrupted with 2-minute notice. Not suitable for production web applications, but excellent for background workers or development environments. You can request a Spot Instance through the EC2 console with the same configuration as On-Demand instances.

Right-Sizing Instances

Monitor CPU and memory usage over weeks. If consistently under 30% utilization, downsize to a smaller instance type. If frequently maxing out, upsize. CloudWatch metrics show utilization patterns. Changing instance types requires stopping the instance (brief downtime), so schedule changes during maintenance windows.

Frequently Asked Questions

Should I use Elastic IP for my EC2 instance?

Yes, for production deployments. Elastic IPs persist even if you stop and restart instances, unlike standard public IPs that change. Allocate an Elastic IP from the EC2 console and associate it with your instance. The first Elastic IP per instance is free; additional IPs or unassociated IPs incur charges (~$0.005/hour).

How do I handle instance restarts without losing data?

Use EBS volumes (which EC2 instances have by default) or named Docker volumes for persistent data. Docker volumes like postgres-data in the compose file persist in /var/lib/docker/volumes/ on the host filesystem, surviving container restarts. For extra durability, enable EBS snapshots or backup to S3.

Can I run multiple applications on one EC2 instance?

Yes, use different ports for each application and configure Nginx virtual hosts to route requests based on domain or path. For example, app1 on port 3000, app2 on port 4000, with Nginx routing app1.com to port 3000 and app2.com to port 4000. Resource limits (CPU/memory) become shared, so monitor total usage.

What happens if my EC2 instance fails?

Without high availability configuration, your application is down until you launch a replacement instance. For critical applications, set up Auto Scaling Groups with multiple instances behind a load balancer, or accept the risk and focus on fast recovery procedures (automated backups, documented restoration steps). Many startups accept single-instance risk initially.

How do I update my application without downtime?

For zero-downtime updates, run two instances behind a load balancer and update one at a time (rolling deployment). For single-instance deployments, downtime during updates is unavoidable but typically under 10 seconds. Schedule updates during low-traffic periods or accept brief interruptions.

Should I run my database on EC2 or use RDS?

RDS offers automated backups, point-in-time recovery, automatic failover, and easier scaling, but costs 2-3x more than self-managed databases on EC2. For small applications where database load is low and budget is tight, PostgreSQL in Docker on EC2 works fine with proper backup procedures. As database demands grow, RDS becomes worth the cost.

How do I scale beyond a single EC2 instance?

Options include vertical scaling (larger instance type), horizontal scaling (multiple instances behind a load balancer), or moving to managed container services (ECS, EKS, Fargate). Horizontal scaling requires refactoring for statelessness—sessions in Redis or databases instead of local memory, uploaded files in S3 instead of local disk.

Can I use this setup for development environments?

Yes, but it's more cost-effective to use t3.micro or t3.small instances for development. Consider using Spot Instances for 90% cost reduction since development environments tolerate interruptions. Tag instances clearly and shut them down when not in use to minimize costs.

How do I rollback a bad deployment?

If you're using versioned Docker images (not :latest), edit docker-compose.yml to reference the previous version tag and run docker compose up -d. This pulls and runs the old version. Automate rollbacks by keeping previous versions deployed side-by-side and switching Nginx routing, or use blue-green deployment strategies.

What's the difference between EC2, ECS, and EKS for Docker deployments?

EC2 gives you raw VMs where you install Docker yourself—maximum control, maximum responsibility. ECS is AWS's container orchestration service—automatic scheduling, load balancing, service discovery, but more complex than EC2. EKS is managed Kubernetes—most powerful, most complex, most expensive. For small projects, EC2 is simplest and cheapest. For large-scale microservices, EKS provides the most capabilities.

Conclusion

Deploying Docker to AWS EC2 provides a cost-effective, flexible production environment for containerized applications. The manual setup investment—configuring instances, installing Docker, setting up reverse proxies, enabling SSL—pays off through full infrastructure control and low operational costs. For applications that don't need automatic scaling or multi-region deployments, this approach delivers production-grade hosting at minimal expense.

Start with a single EC2 instance running Docker Compose. Configure monitoring, automated backups, and deployments. As traffic grows, evaluate whether vertical scaling (larger instance), horizontal scaling (multiple instances), or managed services better fit your needs. The single-instance approach isn't a permanent architecture—it's a pragmatic starting point that validates product-market fit before investing in complex infrastructure.

Focus on delivering value to users first. Infrastructure sophistication can follow success rather than precede it.


Share on Social Media: