How to Deploy Docker to AWS EC2 Step by Step
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.
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.
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.
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.