Seamless Docker Deployment on AWS EC2: An Engineer’s Practical Reference
Cloud-native doesn’t always mean orchestration at scale. Sometimes, direct control is essential—whether for hybrid migrations, systems with brittle network dependencies, or running bespoke workloads with exact kernel requirements. EC2 with Docker offers this control, but deployments are only as robust as their weakest operational detail.
EC2 for Docker: Rationale and Constraints
Why not just use ECS, Fargate, or Kubernetes? In certain cases, teams need:
- Custom OS images for legacy libraries or device drivers.
- Persistent network identities or static EIPs for whitelisted partners.
- Tight coupling with other EC2-bound services.
- Non-standard VPC, IAM, or EBS configurations.
But direct responsibility means direct risk: patching, cgroup accounting, and SIGTERM handling become your concern.
Launch EC2: Practical Steps
Spin up the instance. Don’t default to t2.micro: for small Node.js or Go applications, t3.medium is the baseline—older families or too-small types throttle CPU credits unpredictably.
AMI selection:
Distribution | Version | Notes |
---|---|---|
Amazon Linux 2 | latest | Fast Docker support, lightweight |
Ubuntu | 22.04 LTS | Larger ecosystem, apt-based workflow |
When configuring the Security Group, open only the absolute minimum—typically TCP 22 for SSH, and the public/external facing port your container exposes (e.g., 80 or 443). Do not open direct Docker API ports (2375/2376).
Side note: Attach an IAM role with only the permissions required for your container. For S3 access, avoid wildcards—use resource-level policies for defense in depth.
Install Docker: No Nonsense Instructions
SSH to your instance:
ssh -i path/to/your-key.pem ec2-user@a.b.c.d
Substitute ubuntu
for Ubuntu-based AMI.
Amazon Linux 2:
sudo yum update -y
sudo amazon-linux-extras enable docker
sudo yum install -y docker
sudo systemctl enable --now docker
sudo usermod -aG docker ec2-user
Ubuntu 22.04:
sudo apt-get update
sudo apt-get install -y docker.io
sudo systemctl enable --now docker
sudo usermod -aG docker ubuntu
Log out and back in, or newgrp docker
, to refresh group membership.
Check everything:
docker version
docker info
Docker engine <24.0 is still common in repos as of mid-2024. If you need BuildKit or Compose v2 integration, consider Docker’s official installation scripts instead of OS packages.
Ship Images: Local Build vs. Remote Pull
Where you build is not trivial. For large monolithic images, local EC2 builds can clog disk I/O or exceed root volume defaults (often <10GiB). Prefer build-and-push from CI/CD runner, then pull.
Typical Dockerfile for a production Node app:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 3000
CMD ["node","server.js"]
Build and push (ideally from CI):
docker build -t yourrepo/myapp:latest .
docker login --username yourrepo
docker push yourrepo/myapp:latest
Note: For AWS ECR, use aws ecr get-login-password
and tag appropriately.
Deploy and Operate the Container
Back on EC2:
docker pull yourrepo/myapp:latest
docker run -d --restart unless-stopped --name myapp -p 80:3000 yourrepo/myapp:latest
Why --restart unless-stopped
? It ensures auto-recovery after reboots but allows you to stop the container for maintenance without it immediately respawning.
To mount persistent storage (critical if the app writes logs or uploads):
docker run -d \
--restart unless-stopped \
--name myapp \
-p 80:3000 \
-v /data/app-logs:/app/logs \
yourrepo/myapp:latest
Automate the Flow: Simple Update Script
For single-instance CI/CD without full-blown agents, a Bash deployer minimizes manual error.
#!/bin/bash
NAME="myapp"
IMAGE="yourrepo/myapp:latest"
echo "Fetching latest image..." >&2
docker pull $IMAGE
if docker ps -q -f name=$NAME >/dev/null; then
echo "Stopping and removing container..." >&2
docker stop $NAME && docker rm $NAME
fi
echo "Launching container..." >&2
docker run -d --restart unless-stopped --name $NAME -p 80:3000 $IMAGE
Known issue: If your image has different EXPOSE or entrypoint on update, check for port clashes and review run args.
Practical Issues and Advanced Notes
-
User Data scripts: Automate setup (Docker install, image pull, run) via EC2 User Data. Example:
#!/bin/bash yum update -y amazon-linux-extras install docker -y systemctl enable --now docker docker pull yourrepo/myapp:latest docker run -d --restart unless-stopped --name myapp -p 80:3000 yourrepo/myapp:latest
-
Health checks: Docker’s
HEALTHCHECK
doesn’t integrate with AWS ELB. Set up an Application Load Balancer probing container endpoints (/healthz
) for real liveness tracking. -
Monitoring: Install
cloudwatch-agent
for CPU/disk metrics, ordocker stats
for per-container use. In production, enable Docker’s log-driver withawslogs
/syslog
for log aggregation. -
Security: Don’t run as root inside containers—use
USER
in Dockerfile. Use minimal base images and prune build dependencies. Never expose the Docker daemon API to 0.0.0.0. -
Upgrades & patching: OS and Docker engine security patches are not automatic—integration with SSM Patch Manager or custom cron scripts is advised.
-
Disk space: Container log growth and orphaned images (see
docker system prune
) are a chronic source of silent failures.
Recap
Deploying Docker directly to EC2 remains a viable path for tightly controlled workloads, single-tenancy, or non-standard infrastructure requirements. Tighten security, script your updates, and monitor actively—AWS offloads hardware but you’re still wearing the sysadmin hat.
In production CI/CD, consider baking custom AMIs with Docker preinstalled, or using HashiCorp Packer to enforce baseline configs and accelerate autoscaling.
Multi-container/compose workloads? ECS or systemd-based supervision (with Restart=on-failure
) is often less brittle than inventing your own orchestrator scripts.
Never treat single-instance patterns as production HA. For that, look to managed container platforms or true distributed designs.
Further reading: audit your user data scripts for secrets, rotate SSH keys on rebuild, and don’t assume instance storage sticks around after termination—or you’re in for an expensive surprise.