Every time I build something and want a client to try it, I need it online fast. Not perfectly architected — just accessible, stable enough for a demo, and easy to update when feedback comes in.
For a long time that meant spinning up a VM, installing everything by hand, and hoping it didn’t break when I pushed a change. Context switching between building the app and managing the server adds up. It’s slow, and it’s the kind of friction that makes you put off sharing early work.
Containers fixed most of that. Package the app once, ship it anywhere. And AWS ECS with Fargate made it easy to run those containers without managing servers at all.
This is Part 1 of a 3-part series:
- Part 1 (this post): Manual setup — ECS cluster, ECR, task definition, and deploying your first container
- Part 2: Automating deployments with GitHub Actions
- Part 3: Infrastructure as code with Pulumi
📺 Prefer to watch? Full walkthrough on YouTube
How It All Fits Together
Before touching the console, here’s the architecture at a glance:
Your App (Docker Image)
↓
Amazon ECR ← private image registry
↓
ECS Task Definition ← blueprint: image URL, CPU, memory, ports
↓
ECS Service ← keeps your task running
↓
ECS Cluster ← the managed space everything runs in
↓
Public IP / URL ← how clients access your app
ECS Cluster — an organised space where your services live. Think of it as the environment, not the server.
Task Definition — the blueprint for your container. Comparable to a Kubernetes pod spec. It tells AWS what image to run, how much CPU and memory to allocate, and what ports to expose.
ECR (Elastic Container Registry) — where your Docker images are stored. Private by default, and it integrates natively with ECS so no extra auth setup is needed.
Fargate — the serverless compute option for ECS. You define what to run; AWS figures out where to run it. No EC2 instances to manage.
Prerequisites
- AWS account with CLI configured (
aws configure) - Docker installed locally
- A simple containerised app — we’re using a Python app for this demo
Step 1: Create the ECS Cluster
Go to the ECS section in the AWS console and click Create Cluster.
- Name it something recognisable (I dropped the random suffix AWS suggests)
- Select AWS Fargate (serverless) as the infrastructure type
That’s it. The cluster is just the container — it doesn’t do anything until you add services to it.
Step 2: Create the ECR Repository
Before building the task definition, get the image repository ready. That way you’ll have the image URL on hand when you need it.
Go to ECR → Create Repository. Give it a name that matches your app.
Once created, copy the repository URI — you’ll use it as the Docker image tag.
# Format:
# <account_id>.dkr.ecr.<region>.amazonaws.com/<repo-name>
# Example:
123456789.dkr.ecr.ap-southeast-1.amazonaws.com/gif-app
Step 3: Build and Push Your Docker Image
For this demo the app is a simple Python web app that displays random GIFs on every page refresh. Nothing fancy — the point is the deployment pattern, not the app.
# Authenticate Docker with ECR
aws ecr get-login-password --region ap-southeast-1 | \
docker login --username AWS --password-stdin \
123456789.dkr.ecr.ap-southeast-1.amazonaws.com
# Build the image using the ECR repo URI as the tag
docker build -t 123456789.dkr.ecr.ap-southeast-1.amazonaws.com/gif-app:latest .
# Run it locally to verify it works
docker run -p 5000:5000 123456789.dkr.ecr.ap-southeast-1.amazonaws.com/gif-app:latest
# Push to ECR
docker push 123456789.dkr.ecr.ap-southeast-1.amazonaws.com/gif-app:latest
If you get an authorization token expired error on push, just re-run the
aws ecr get-login-passwordcommand and try again.
Step 4: Create the Task Definition
Go to ECS → Task Definitions → Create new task definition.
Key fields to fill in:
| Field | Value |
|---|---|
| Launch type | AWS Fargate |
| CPU | 0.25 vCPU (enough for a PoC) |
| Memory | 0.5 GB |
| Image URI | your ECR repo URI + :latest |
| Port mapping | 5000 (or whatever port your app listens on) |
Task definitions are versioned — every change creates a new revision. This makes it easy to roll back or pick a specific version when deploying a service.
Leave the rest as defaults for now.
Step 5: Create the ECS Service
Now go back to your cluster and create a service.
The service is what keeps your task running. If the container crashes, the service restarts it. If you want multiple copies, the service manages that too.
Under Networking, the security group is the important part here. For a PoC, open inbound traffic on port 5000 (or whichever port your app uses). You’d tighten this down for production.
Also make sure Public IP is enabled — this is how you’ll access the app.
Create the service and wait for the task to reach a RUNNING state.
Step 6: Access Your App
Go to your cluster → Tasks → click the running task → find the Public IP.
Open http://<public-ip>:5000 in your browser.
The IP changes every time you create a new task or deploy an update. For a quick client demo this is fine. For anything more permanent, you’d put a load balancer in front — that’s covered in a later post.
Step 7: Push an Update
This is the part that used to be painful. Here’s how it works now.
Make your code change, rebuild, and push:
docker build -t 123456789.dkr.ecr.ap-southeast-1.amazonaws.com/gif-app:latest .
docker push 123456789.dkr.ecr.ap-southeast-1.amazonaws.com/gif-app:latest
Check ECR — you should now see 2 images in the repository (latest will be overwritten, but the previous image is retained with its digest).
Back in ECS, go to your service and click Force new deployment. ECS will pull the latest image and replace the running task. Get the new IP and verify the change is live.
What You’ve Built
At this point you have:
- A private Docker image registry (ECR)
- A serverless container running your app (ECS Fargate)
- A repeatable push-and-redeploy workflow
It’s not automated yet — every update still requires a manual docker push and a forced redeployment. That’s what Part 2 fixes with GitHub Actions.
What’s Next
Part 2 wires this up to GitHub Actions so that every push to main automatically builds, pushes to ECR, and triggers an ECS redeployment. No console clicks required.
Part 3 replaces the manual console setup with Pulumi so the whole infrastructure is reproducible in code.
Questions or issues? Drop them in the comments.