Secure by Default: Deploying a Containerized Node.js App on AWS with ECR, Fargate, and Secrets Manager
A practical walkthrough of securing a containerized web app on AWS: hardening the Docker image, scanning in Amazon ECR, keeping secrets out of code, using least-privilege task roles, isolating networks on ECS Fargate, and deploying from CI/CD without long-lived access keys.
Getting a container running on AWS is the easy part. Getting it running securely is where most projects fall short. A Docker image with a leaked secret, a task with an admin role, or a service open to the whole internet can undo everything else you built.
This article walks through a security-first path for deploying a Node.js web app on Amazon ECS with AWS Fargate, covering the image, the registry, secrets, permissions, the network, and the pipeline. The same ideas apply to any language or framework.
1. Harden the Docker Image
Security starts before anything reaches AWS. A smaller, stricter image means fewer vulnerabilities and less for an attacker to use.
dockerfile
# Build stageFROM node:22-alpine AS buildWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build# Runtime stageFROM node:22-alpineWORKDIR /appENV NODE_ENV=productionCOPY --from=build /app/dist ./distCOPY package*.json ./RUN npm ci --omit=dev && npm cache clean --forceUSER nodeEXPOSE 3000CMD ["node", "dist/server.js"]
What this does:
- Multi-stage build: compilers and dev dependencies never reach the final image.
- Minimal base image: fewer packages means a smaller attack surface.
- Non-root user: if the app is compromised, the attacker doesn't get root inside the container.
- No secrets baked in: never
COPYa.envfile into an image. Add it to.dockerignore.
Pin your base image to a specific version (or digest) instead of
latest , so builds are repeatable and you decide when to upgrade.2. Scan Images in Amazon ECR
Amazon ECR can scan images for known vulnerabilities. Turn it on, and block the deployment if serious findings appear.
- Enable scan on push for basic scanning, or enable enhanced scanning through Amazon Inspector for continuous scanning as new CVEs are published.
- Turn on tag immutability so a tag like
v1.4.2can't be silently overwritten with a different image.
- Add lifecycle policies to remove old images you no longer need.
Bash
aws ecr create-repository \--repository-name my-node-app \--image-scanning-configuration scanOnPush=true \--image-tag-mutability IMMUTABLE
A vulnerability in a base image or in the language runtime is fixed by rebuilding and redeploying a patched image, not by updating your application code. Scanning on a schedule helps you notice when that's needed.
3. Keep Secrets Out of the Image and the Repo
Database passwords and API keys belong in AWS Secrets Manager or SSM Parameter Store. ECS can inject them into the container as environment variables at startup, so they never appear in your code or image.
In your task definition:
JSON
"secrets": [{"name": "DB_PASSWORD","valueFrom": "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:prod/my-app/db-AbCdEf"}]
If a secret is ever committed to Git, treat it as compromised and rotate it, even if the repository is private. Enable automatic rotation in Secrets Manager where your service supports it.
4. Use Two IAM Roles, Each With Least Privilege
ECS tasks use two different roles, and mixing them up is a common mistake:
Role | Used by | Typical permissions |
|---|---|---|
Task execution role | ECS agent, to start the task | Pull the image from ECR, write logs to CloudWatch, read the specific secrets it injects |
Task role | Your application code, while running | Only what the app needs, such as reading one S3 prefix |
Keep the task role small. If the app only reads from one bucket prefix, grant only that:
JSON
{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Action": ["s3:GetObject"],"Resource": "arn:aws:s3:::my-app-data/uploads/*"}]}
Avoid attaching
AdministratorAccess or wildcard policies "just to make it work." Use IAM Access Analyzer to review what your roles actually use, then tighten them.5. Lock Down the Network
A secure layout for a public web app looks like this:
- Application Load Balancer in public subnets, the only thing exposed to the internet.
- Fargate tasks in private subnets with no public IP addresses.
- Security groups that chain together: the task's security group allows inbound traffic only from the load balancer's security group, on the app port, and nothing else.
- VPC endpoints for ECR, S3, CloudWatch Logs, and Secrets Manager, so tasks can pull images and fetch secrets without sending traffic over the public internet.
- AWS WAF on the load balancer to filter common attacks like SQL injection, cross-site scripting, and abusive bots.
- HTTPS everywhere, using certificates from AWS Certificate Manager, with HTTP redirected to HTTPS.
Never open SSH or other management ports to
0.0.0.0/0 . Fargate has no host to SSH into, and ECS Exec (backed by SSM) lets you open a shell in a running container when you truly need one, with every session controlled by IAM and loggable.6. Tighten the Container at Runtime
In the container definition, a few settings add real protection:
JSON
"readonlyRootFilesystem": true,"user": "1000","linuxParameters": {"initProcessEnabled": true}
A read-only root filesystem prevents an attacker from writing tools or modifying the app inside the container. If your app needs to write temporary files, mount a dedicated writable volume for just that path.
7. Deploy From CI/CD Without Long-Lived Keys
The pipeline is a common target because it holds powerful credentials. Avoid storing AWS access keys as CI secrets. Instead, use OIDC federation so your pipeline (for example, GitHub Actions) assumes a short-lived IAM role:
YAML
permissions:id-token: writecontents: readsteps:- uses: aws-actions/configure-aws-credentials@v4with:role-to-assume: arn:aws:iam::<ACCOUNT_ID>:role/github-deploy-roleaws-region: <REGION>
Then scope that deploy role tightly: push to one ECR repository and update one ECS service, nothing more. Add these checks before every deploy:
- Dependency audit (
npm audit) and secret scanning on each pull request.
- Image build, then ECR or Inspector scan.
- Fail the pipeline on critical or high findings.
- Deploy only from the main branch, after review.
8. Log, Monitor, and Alert
- Send container logs to Amazon CloudWatch Logs and set a retention period.
- Enable AWS CloudTrail to record who changed what in your account.
- Turn on Amazon GuardDuty, which includes runtime and network threat detection.
- Create CloudWatch alarms for error spikes, unusual traffic, and task failures.
- Set AWS Budgets alerts, because unexpected cost jumps can also signal abuse, such as a compromised account running crypto-mining workloads.
Quick Checklist
- [ ] Multi-stage, minimal image running as a non-root user
- [ ] No secrets in the image, repo, or environment files
- [ ] ECR scan on push, tag immutability, and a patch-and-rebuild process
- [ ] Secrets injected from Secrets Manager or Parameter Store
- [ ] Separate, least-privilege task execution role and task role
- [ ] Tasks in private subnets, security groups chained to the load balancer
- [ ] AWS WAF and HTTPS on the public endpoint
- [ ] Read-only root filesystem where possible
- [ ] CI/CD using OIDC, with no long-lived access keys
- [ ] CloudTrail, GuardDuty, CloudWatch alarms, and Budgets enabled
Conclusion
Secure container deployment isn't one big step. It's a series of small, deliberate defaults: a lean image, no hard-coded secrets, narrow permissions, a private network, and a pipeline that doesn't hold permanent keys. None of them is difficult on its own, and together they make an attacker's job much harder.
Start with the changes that cost the least, which are the non-root user, Secrets Manager, and ECR scanning, then layer in the network and pipeline controls as your project grows.
How do you manage secrets in your container deployments: Secrets Manager, Parameter Store, or something else? Share your approach in the comments.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article