How to Start a Docker Host on AWS EC2: Complete Deployment and Configuration Guide

How to Start a Docker Host on AWS EC2

EC2 is where a huge share of the world’s Docker containers actually run, whether directly on bare EC2 instances or underneath ECS and EKS. This guide walks through provisioning a Docker-ready EC2 instance from the ground up — networking, security groups, installation, TLS-secured remote access, and persistent storage — using nothing but the AWS CLI, so every step is scriptable and repeatable.

Prerequisites

  • An AWS account
  • AWS CLI configured (aws configure), or run via container (see the companion guide on cloud CLIs in Docker)
  • An SSH key pair

Step 1: Create a Key Pair

aws ec2 create-key-pair --key-name docker-key --query 'KeyMaterial' --output text > docker-key.pem
chmod 400 docker-key.pem

Step 2: Create a Security Group

Security groups are AWS’s equivalent of a stateful firewall attached directly to instances.

aws ec2 create-security-group \
  --group-name docker-sg \
  --description "Docker host security group"

Expected output:

{
    "GroupId": "sg-0a1b2c3d4e5f6a7b8"
}

Allow SSH, HTTP, and (scoped) the Docker API port:

aws ec2 authorize-security-group-ingress \
  --group-id sg-0a1b2c3d4e5f6a7b8 \
  --protocol tcp --port 22 --cidr YOUR_IP/32

aws ec2 authorize-security-group-ingress \
  --group-id sg-0a1b2c3d4e5f6a7b8 \
  --protocol tcp --port 80 --cidr 0.0.0.0/0

aws ec2 authorize-security-group-ingress \
  --group-id sg-0a1b2c3d4e5f6a7b8 \
  --protocol tcp --port 2376 --cidr YOUR_IP/32

Step 3: Launch the Instance

Use Amazon Linux 2023’s built-in dnf package or Ubuntu with a user-data script — I’ll use Ubuntu here since it maps cleanly to the same install steps used across the Azure and GCP guides in this series.

cat > user-data.sh << 'EOF'
#!/bin/bash
apt-get update -y
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
usermod -aG docker ubuntu
systemctl enable docker
systemctl start docker
EOF

aws ec2 run-instances \
  --image-id ami-0e86e20dae9224db8 \
  --instance-type t3.small \
  --key-name docker-key \
  --security-group-ids sg-0a1b2c3d4e5f6a7b8 \
  --user-data file://user-data.sh \
  --count 1 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=docker-host-01}]'

Note: the AMI ID above is region-specific and changes over time. Look up the current Ubuntu 24.04 LTS AMI for your region with:

aws ec2 describe-images \
  --owners 099720109477 \
  --filters "Name=name,Values=ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*" \
  --query 'sort_by(Images, &CreationDate)[-1].ImageId' \
  --output text

Step 4: Get the Public IP and Connect

aws ec2 describe-instances \
  --filters "Name=tag:Name,Values=docker-host-01" \
  --query 'Reservations[0].Instances[0].PublicIpAddress' \
  --output text
ssh -i docker-key.pem ubuntu@<PUBLIC_IP>
docker --version
docker run hello-world

Expected output:

Docker version 27.3.1, build ce12230
Hello from Docker!
This message shows that your installation appears to be working correctly.

Step 5: Secure Remote Docker API Access with TLS

If you want to control this Docker daemon remotely from your laptop rather than SSHing in each time, generate CA and server certificates on the instance:

mkdir -p ~/docker-certs && cd ~/docker-certs
openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=$(hostname)" -new -key server-key.pem -out server.csr
echo subjectAltName = IP:<PUBLIC_IP>,IP:127.0.0.1 > extfile.cnf
openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
  -CAcreateserial -out server-cert.pem -extfile extfile.cnf

Configure /etc/docker/daemon.json:

{
  "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
  "tls": true,
  "tlsverify": true,
  "tlscacert": "/etc/docker/ca.pem",
  "tlscert": "/etc/docker/server-cert.pem",
  "tlskey": "/etc/docker/server-key.pem"
}
sudo systemctl restart docker

On your laptop:

export DOCKER_HOST=tcp://<PUBLIC_IP>:2376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH=~/docker-certs/client
docker ps

Step 6: Attach an EBS Volume for Persistent Storage

aws ec2 create-volume \
  --availability-zone us-east-1a \
  --size 100 \
  --volume-type gp3

aws ec2 attach-volume \
  --volume-id vol-0abc123def456 \
  --instance-id i-0123456789abcdef0 \
  --device /dev/sdf

On the instance:

sudo lsblk
sudo mkfs -t ext4 /dev/xvdf
sudo mkdir /mnt/docker-data
sudo mount /dev/xvdf /mnt/docker-data

Add "data-root": "/mnt/docker-data/docker" to daemon.json and restart Docker so images and containers live on the EBS volume rather than the (typically smaller) root EBS volume.

Networking Fundamentals on EC2

EC2 instances by default get a private IP within a VPC subnet and, if launched in a public subnet, an ephemeral public IP mapped via the Internet Gateway. Security groups act at the instance’s network interface level — this is a different layer entirely from Docker’s own bridge networking. A container listening on port 80 inside the instance is invisible to the outside world until both (a) you’ve published that port to the host with -p 80:80, and (b) the security group allows inbound traffic on port 80. Missing either one is the most common reason a freshly deployed container “isn’t working” on a first EC2 deployment.

Running a Real Workload

docker run -d --name web-demo -p 80:80 --restart unless-stopped nginx:latest

Visit http://<PUBLIC_IP> in a browser to confirm.

Monitoring

docker stats --no-stream

For production visibility, install the Amazon CloudWatch agent, which can be configured to collect Docker container-level metrics (CPU, memory, disk) alongside standard EC2 host metrics:

sudo apt-get install -y amazon-cloudwatch-agent
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
  -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/etc/config.json -s

Deploying a Multi-Container Application with Compose

Install the Compose plugin (bundled with the get-docker.sh script used earlier) and define your stack:

# docker-compose.yml
services:
  web:
    image: nginx:latest
    ports:
      - "80:80"
    restart: unless-stopped

  api:
    image: myregistry.example.com/api:1.0.0
    environment:
      - DATABASE_URL=postgresql://appuser:apppass@db:5432/appdb
    restart: unless-stopped

  db:
    image: postgres:16
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=appdb
      - POSTGRES_USER=appuser
      - POSTGRES_PASSWORD=apppass
    restart: unless-stopped

volumes:
  db-data:
docker compose up -d

Expected output:

[+] Running 4/4
 ✔ Network ubuntu_default   Created
 ✔ Container ubuntu-db-1    Started
 ✔ Container ubuntu-api-1   Started
 ✔ Container ubuntu-web-1   Started

High Availability with an Auto Scaling Group and Load Balancer

A single EC2 instance is a single point of failure. For production traffic, wrap the instance configuration in a launch template and an Auto Scaling Group behind an Application Load Balancer:

aws ec2 create-launch-template \
  --launch-template-name docker-host-template \
  --version-description v1 \
  --launch-template-data '{
    "ImageId": "ami-0e86e20dae9224db8",
    "InstanceType": "t3.small",
    "KeyName": "docker-key",
    "SecurityGroupIds": ["sg-0a1b2c3d4e5f6a7b8"],
    "UserData": "'"$(base64 -w0 user-data.sh)"'"
  }'

aws autoscaling create-auto-scaling-group \
  --auto-scaling-group-name docker-asg \
  --launch-template LaunchTemplateName=docker-host-template,Version='$Latest' \
  --min-size 2 --max-size 4 --desired-capacity 2 \
  --vpc-zone-identifier "subnet-0123abc,subnet-0456def"

AWS will replace unhealthy instances automatically based on the ASG’s health check configuration, giving you self-healing infrastructure without introducing a full orchestrator.

Understanding EC2’s Networking Layers

As with every cloud, it’s worth keeping these layers distinct in your head:

  • VPC and subnet — the private IP address space the instance lives in, and whether it’s public or private.
  • Security group — the stateful firewall attached to the instance’s network interface; this is what aws ec2 authorize-security-group-ingress modifies.
  • Docker’s internal bridge network — entirely invisible to AWS, handles container-to-container traffic on the instance.
  • Published ports (-p) — the bridge connecting the instance’s public IP to a container’s internal port.

Every one of these layers needs to be correctly configured for a container to be reachable from outside — a green docker ps output guarantees nothing about external reachability on its own.

Best Practices

  1. Use an IAM instance role rather than embedding AWS access keys on the instance — the EC2 metadata service (IMDSv2) provides temporary credentials automatically.
  2. Enable IMDSv2 only (--metadata-options HttpTokens=required) to protect against SSRF-based credential theft.
  3. Scope security groups tightly — port 2376 should never be open to 0.0.0.0/0.
  4. Use gp3 EBS volumes over gp2 for better baseline IOPS at lower cost for Docker’s storage-heavy workloads.
  5. Consider ECS or EKS instead of raw EC2 if you’re managing more than a handful of services — they remove daemon and patching overhead entirely while still running on EC2 under the hood.
  6. Tag everything (Environment, Team, Project) for cost allocation from the start.

Choosing an Instance Type

Instance sizing is one of the most common early mistakes on EC2 Docker hosts. t3/t3a burstable instances are fine for low, spiky traffic (dev environments, small internal tools) but can throttle under sustained CPU load once their CPU credit balance runs out — check this with:

aws cloudwatch get-metric-statistics \
  --namespace AWS/EC2 --metric-name CPUCreditBalance \
  --dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
  --start-time 2026-07-29T00:00:00Z --end-time 2026-07-30T00:00:00Z \
  --period 3600 --statistics Average

For sustained, predictable container workloads, m6i or m7g (Graviton, ARM-based — note that container images need arm64 variants) instances give consistent performance without credit throttling. Memory-heavy workloads like databases or caching layers are usually better served by the r-family instances.

Troubleshooting

  • Can’t SSH in — check the security group allows port 22 from your current IP; note that many home/office IPs change over time, so YOUR_IP/32 rules need periodic updates.
  • docker run hello-world fails right after boot — user-data scripts can take a minute or two to finish; check /var/log/cloud-init-output.log for errors.
  • Public IP is empty — confirm the instance was launched in a public subnet with auto-assign public IP enabled, or that you attached an Elastic IP.
  • EBS volume not showing under lsblk — confirm the --device name matches what the instance type actually exposes; Nitro-based instances often rename /dev/sdf to /dev/xvdf or an NVMe device path.

Summary

A Docker host on EC2 comes down to four building blocks: a security group controlling what’s reachable, an instance with Docker installed via user-data, TLS certificates if you want remote API access, and an EBS volume if you need storage beyond the root disk. None of this is EC2-specific once Docker itself is running — the container networking, image layering, and daemon behavior are identical to any other Linux host; only the surrounding provisioning and security group layer changes from cloud to cloud.

References

  • Amazon EC2 documentation: https://docs.aws.amazon.com/ec2/
  • Docker Engine installation guide: https://docs.docker.com/engine/install/ubuntu/
  • Docker Engine API security (TLS): https://docs.docker.com/engine/security/protect-access/
  • AWS IMDSv2 documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
Total
0
Shares

Leave a Reply

Previous Post
How to Access Public Clouds to Run Docker

How to Access Public Clouds to Run Docker: AWS, Azure, and GCP Deployment Options Explained

Next Post
How to Start a Docker Host on Google GCE

How to Start a Docker Host on Google GCE: Complete Setup and Configuration Guide

Related Posts