Dockerizing an Application and Pushing the Image to ECR
In this article we package an application into a Docker image, then push that image to ECR — AWS's image registry. This is the prep step for Article 7, where we automate the whole build-and-deploy process.
Recall from Article 0: Docker packages both code and environment into an image, so the application runs the same everywhere. ECR (Elastic Container Registry) is where those images are stored on AWS, for EC2 or other services to pull and run.
Goals
- Write a small Node.js application and a Dockerfile for it.
- Build the image and test it locally.
- Create an ECR repository.
- Log in to ECR, tag, and push the image.
- Clean up the repository.
Expected cost
- ECR charges by the storage of images held and the amount of data pulled out. A small Node.js image is only a few dozen MB.
- Legacy-model account (before 2025-07-15): ECR gives 500 MB of storage free per month for the first 12 months.
- Credit-model account: this small cost is deducted from credit.
The cost is nearly negligible, but we still delete the repository at the end to keep the habit.
This article needs Docker installed locally. Check with docker --version; if you don't have it, install Docker Desktop (macOS/Windows) or Docker Engine (Linux).
Step 1: Write the application and Dockerfile
Create a project directory with a simple Express app:
mkdir todo-app && cd todo-app
Create package.json:
{
"name": "todo-app",
"version": "1.0.0",
"main": "server.js",
"scripts": { "start": "node server.js" },
"dependencies": { "express": "^4.19.2" }
}
Create server.js:
const express = require("express");
const app = express();
const PORT = process.env.PORT || 3000;
app.get("/", (req, res) => {
res.send("<h1>Todo app chay trong Docker container</h1>");
});
app.get("/health", (req, res) => {
res.json({ status: "ok" });
});
app.listen(PORT, () => {
console.log(`Server dang chay tren cong ${PORT}`);
});
Create the Dockerfile:
# Use the official Node image as the base
FROM node:20-alpine
# Working directory inside the container
WORKDIR /app
# Copy the dependency manifest first to take advantage of caching
COPY package.json ./
RUN npm install --omit=dev
# Copy the rest of the code
COPY . .
# The app listens on port 3000
EXPOSE 3000
# Command to run when the container starts
CMD ["npm", "start"]
Add a .dockerignore so you don't package unnecessary things into the image:
node_modules
npm-debug.log
.git
The order in the Dockerfile is deliberate: copy package.json and install dependencies first, copy the code after. Docker caches each step, so when you only change code without changing dependencies, the npm install step is reused from cache and the build is faster.
Step 2: Build and test locally
Build the image, naming it todo-app:
docker build -t todo-app .
Run it, mapping the container's port 3000 to the machine's port 3000:
docker run -p 3000:3000 todo-app
Open http://localhost:3000 in a browser and you'll see the app's page. Try http://localhost:3000/health, which returns JSON. Press Ctrl+C in the terminal to stop the container.
An image that runs on your machine will run exactly the same anywhere Docker is present, including on EC2.
Step 3: Create an ECR repository
Each application usually gets its own repository on ECR. Create a repository named todo-app:
aws ecr create-repository \
--repository-name todo-app \
--region ap-southeast-1 \
--query "repository.repositoryUri" --output text
The command returns the repository URI, of the form <account-id>.dkr.ecr.ap-southeast-1.amazonaws.com/todo-app. Save this URI. Grab the account id and set a few variables for convenience:
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=ap-southeast-1
REPO_URI=$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/todo-app
echo $REPO_URI
Step 4: Log in to ECR and push the image
ECR is a private registry, so Docker needs to log in before pushing. Get the login token from AWS and hand it to Docker:
aws ecr get-login-password --region $REGION \
| docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com
This command gets a temporary token (valid for 12 hours) and logs Docker into your ECR registry. When you see Login Succeeded, you're set.
The image on your machine is named todo-app, but to push it to ECR it needs to be tagged with the correct repository URI:
docker tag todo-app:latest $REPO_URI:latest
Push to ECR:
docker push $REPO_URI:latest
Check that the image is on ECR:
aws ecr list-images --repository-name todo-app \
--query "imageIds[].imageTag" --output table
Seeing the latest tag in the list means the image is on ECR. From here, EC2 or AWS's container services can pull this image and run it — exactly what Article 7 will automate.
About the
latesttag: convenient for learning, but in practice you should tag images by a specific version (for example by Git commit hash) so you know exactly which build is running and can roll back when needed. We'll do it this proper way in Article 7.
🧹 Cleanup
ECR charges by the storage of images held, so delete the repository (along with all images inside it):
aws ecr delete-repository \
--repository-name todo-app \
--region ap-southeast-1 \
--force
--force allows deleting even when the repository still has images inside. Check it's deleted:
aws ecr describe-repositories \
--query "repositories[].repositoryName" --output table
Locally, you can delete the Docker images you built to free up disk (optional):
docker rmi todo-app $REPO_URI:latest
If you plan to do Article 7 right after this one, you can keep the
todo-appdirectory and code, since Article 7 reuses this same application. Just deleting the ECR repository is enough to avoid incurring cost; the repository will be recreated in Article 7.
Wrap-up
You just packaged an application into a Docker image, tested it, then pushed it to ECR. This build → tag → push workflow is the core operation that every CI/CD pipeline performs, the only difference being it's automated instead of typed by hand.
Article 7 ties everything together: setting up GitHub Actions so that every time you push code to Git, the system automatically builds the image, pushes it to ECR, and deploys to EC2 — with no manual steps left.