Deploy a Node.js app on AWS
Deploy a containerized Node.js application on AWS ECS Fargate with a production-oriented setup. This guide covers architecture choices, networking, secrets, health checks, HTTPS, and safe updates.
Choose an AWS deployment path that fits your application
To deploy a node.js app on aws, you need more than a running server: you need a repeatable release process, secure configuration, reliable routing, and enough visibility to diagnose failures. This MyDiscussions tutorial uses Amazon ECS with AWS Fargate, a practical choice for teams deploying Express, Fastify, NestJS, or other containerized Node.js services without managing EC2 hosts.
The target architecture is straightforward: an Application Load Balancer (ALB) receives requests and forwards them to Node.js containers running as ECS tasks. Amazon Elastic Container Registry (ECR) stores container images, CloudWatch collects logs, and AWS Secrets Manager supplies sensitive configuration.
This is a production-oriented foundation, not a complete compliance or disaster-recovery design. You should finish with a working service and understand which infrastructure decisions require further investment.
Compare the main AWS hosting options
Choose the deployment model before writing infrastructure code. Runtime behavior, operational ownership, and existing team skills matter more than convenience during the first deployment.
| AWS option | Best fit | Main trade-off |
|---|---|---|
| ECS with Fargate | Containerized APIs, web services, and background workers | No host management, but networking and service configuration remain your responsibility |
| Elastic Beanstalk | Conventional web applications needing managed environment provisioning | Faster initial setup, with platform conventions to understand |
| Lambda with API Gateway | Event-driven APIs and workloads with intermittent demand | Execution limits, cold starts, and different application architecture |
| EC2 | Applications requiring host-level control or unusual system dependencies | Your team manages patching, capacity, and server recovery |
| EKS | Organizations already standardized on Kubernetes | Significant Kubernetes operational complexity |
Use ECS Fargate here if your application runs continuously, already supports Docker, or needs a predictable HTTP server lifecycle.
Consider Lambda instead when requests trigger independent, short-lived operations and avoiding continuously running capacity matters. Choose EKS only when Kubernetes capabilities or organizational standards justify it—not simply because containers are involved.
Define prerequisites and the deployment boundary
You will need:
- An AWS account and a deployment identity with appropriate permissions.
- AWS CLI v2, Docker, and a supported Node.js LTS release.
- A domain you control for production HTTPS.
- A selected AWS Region used consistently throughout the tutorial.
- A stateless Node.js application, or a plan to externalize state.
Authenticate through AWS IAM Identity Center or another short-lived credential mechanism where possible. Avoid creating permanent administrator access keys for routine deployments.
Verify your active identity:
```bash
aws sts get-caller-identity
export AWS_REGION=us-east-1
export APP_NAME=mydiscussions-node
export AWS_ACCOUNT_ID=$(
aws sts get-caller-identity --query Account --output text
)
```
The sample uses port 3000. Store uploads in S3, shared sessions in a suitable external store, and application data in a managed database such as Amazon RDS. Container-local storage is not durable application storage.
Step 1: Prepare Node.js for container deployment
Create a minimal Express application:
```bash
mkdir mydiscussions-node
cd mydiscussions-node
npm init -y
npm install express
npm pkg set scripts.start="node server.js"
```
Create server.js:
```javascript
const express = require("express");
const app = express();
const port = Number(process.env.PORT || 3000);
app.get("/health", (_req, res) => {
res.status(200).json({ status: "ok" });
});
app.get("/", (_req, res) => {
res.json({ message: "Node.js running on AWS" });
});
const server = app.listen(port, "0.0.0.0", () => {
console.log(JSON.stringify({ event: "listening", port }));
});
process.on("SIGTERM", () => {
console.log(JSON.stringify({ event: "shutdown_started" }));
server.close(() => {
process.exit(0);
});
setTimeout(() => process.exit(1), 25000).unref();
});
```
Binding to 0.0.0.0 allows traffic to reach the process through the container network. Binding only to localhost will prevent the load balancer from connecting.
The shutdown handler gives in-flight requests time to finish during deployments. Real applications should also close database connections and stop consuming background jobs.
Keep /health fast and unauthenticated. Avoid making it depend on every downstream service: a shared dependency outage could otherwise cause ECS to replace healthy processes repeatedly.
Step 2: Build and test a production container
Create a Dockerfile:
```dockerfile
FROM node:24-bookworm-slim
ENV NODE_ENV=production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --chown=node:node server.js ./
USER node
EXPOSE 3000
CMD ["node", "server.js"]
```
Node.js 24 is an LTS line at the time of this guide. Check framework compatibility before adopting any runtime version. For stricter reproducibility, pin the base image by digest and automate security updates.
Create .dockerignore:
```text
node_modules
.git
.env
.env.*
npm-debug.log
```
Build and test:
```bash
docker build --platform linux/amd64 -t "$APP_NAME:local" .
docker run --rm -p 3000:3000 "$APP_NAME:local"
```
From another terminal:
```bash
curl --fail http://localhost:3000/health
```
This tutorial uses an x86-64 image and matching ECS runtime architecture. An ARM-based development laptop may otherwise produce an image incompatible with the selected task configuration.
For TypeScript or NestJS, use a multi-stage Docker build: compile with development dependencies, then copy compiled output and production dependencies into the runtime image.
Step 3: Push the image to Amazon ECR
Create a private repository with immutable tags:
```bash
aws ecr create-repository \
--repository-name "$APP_NAME" \
--image-tag-mutability IMMUTABLE \
--region "$AWS_REGION"
export REGISTRY="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
export IMAGE_TAG="v1"
export IMAGE_URI="$REGISTRY/$APP_NAME:$IMAGE_TAG"
aws ecr get-login-password --region "$AWS_REGION" |
docker login --username AWS --password-stdin "$REGISTRY"
docker tag "$APP_NAME:local" "$IMAGE_URI"
docker push "$IMAGE_URI"
```
Use a unique tag for every release, preferably a Git commit identifier. Do not overwrite latest and expect a running ECS service to update automatically.
Configure ECR image scanning and an image-retention policy. Keep enough previous releases for rollback, and do not expire images still referenced by active task definitions.
Step 4: Configure networking and security boundaries
For production, use a VPC spanning at least two Availability Zones:
- Two public subnets for the internet-facing ALB.
- Two private subnets for Fargate tasks.
- An internet gateway serving the public subnet routes.
- An outbound connectivity plan for private tasks.
Private tasks need access to services used during startup, including ECR and CloudWatch Logs. A NAT gateway provides general outbound access. Alternatively, appropriate VPC endpoints can provide private access to AWS services; pulling ECR images also involves S3 connectivity.
Endpoints do not replace internet access for arbitrary third-party APIs.
Create two security groups:
- ALB security group: inbound TCP
80and443from intended clients. - Task security group: inbound TCP
3000only from the ALB security group.
For this setup, allow outbound traffic and tighten it later based on documented dependencies. Keep task public IP assignment disabled.
A sandbox can place tasks in public subnets with public IPs while retaining ALB-only inbound access. That reduces networking setup, but it should be a deliberate architecture choice rather than an accidental production default.
Step 5: Create IAM roles, logs, and configuration
ECS uses two different IAM roles:
- Task execution role: lets ECS pull images, publish logs, and retrieve configured secrets.
- Task role: grants permissions to application code running inside the container.
Create both with a trust policy for ecs-tasks.amazonaws.com. Attach the AWS-managed AmazonECSTaskExecutionRolePolicy to the execution role. Grant the application task role only the actions it actually needs.
Create the log group:
```bash
aws logs create-log-group \
--log-group-name "/ecs/$APP_NAME" \
--region "$AWS_REGION"
aws logs put-retention-policy \
--log-group-name "/ecs/$APP_NAME" \
--retention-in-days 30 \
--region "$AWS_REGION"
```
For a real application, place credentials in Secrets Manager and reference them through the task definition’s Secrets configuration. Grant the execution role secretsmanager:GetSecretValue for the specific secret, plus any required customer-managed KMS key permissions.
Never bake .env files into images. Also remember that secrets injected as environment variables are read when the task starts: rotating a secret does not update already-running containers automatically.
Step 6: Register an ECS task definition
In the ECS console, create a task definition with these settings:
| Setting | Initial value |
|---|---|
| Launch type compatibility | AWS Fargate |
| Operating system / architecture | Linux / X86_64 |
| Network mode | awsvpc |
| Task CPU | 0.25 vCPU |
| Task memory | 0.5 GB |
| Container name | app |
| Image | Your complete ECR image URI |
| Container port | 3000 |
| Environment | NODE_ENV=production, PORT=3000 |
| Logs | awslogs, using /ecs/mydiscussions-node |
Assign the execution and task roles created earlier. Configure the log Region and a stream prefix such as ecs.
These CPU and memory values are a starting point for a small service, not a sizing recommendation for every workload. Server-side rendering, large JSON responses, and memory-intensive libraries can require substantially more capacity.
Review the official ECS task definition parameters when converting the console configuration into infrastructure as code.
Step 7: Create the load balancer and ECS service
Create an internet-facing Application Load Balancer in the two public subnets, using its dedicated security group.
Create an HTTP target group with:
- Target type: IP, not instance.
- Target port:
3000. - Health check path:
/health. - Success code:
200.
Create an HTTP listener on port 80 forwarding to that target group. Leave manual target registration empty; ECS will register task IP addresses.
Next, create an ECS cluster and a service:
- Select the registered task definition.
- Choose Fargate compute.
- Set desired task count to two.
- Select the private subnets and task security group.
- Disable public IP assignment.
- Attach the ALB target group to container
app, port3000. - Set a health-check grace period appropriate to startup time.
- Enable the deployment circuit breaker with rollback.
Two tasks across Availability Zones provide a better availability baseline than one. They do not protect against every shared dependency failure.
Wait for both tasks to become healthy, then test:
```bash
curl --fail http://YOUR_ALB_DNS_NAME/health
```
If deployment fails, inspect ECS service events, stopped-task reasons, target health details, and CloudWatch logs—in that order.
Step 8: Add HTTPS and your domain
Request a public certificate in AWS Certificate Manager, in the same Region as the ALB. Complete DNS validation using the records ACM supplies.
Then:
- Add an ALB HTTPS listener on port
443. - Associate the validated certificate.
- Forward HTTPS traffic to the application target group.
- Change the port
80listener to redirect to HTTPS. - Create a Route 53 alias record pointing your hostname to the ALB.
Other DNS providers work too, using supported alias-style records or a CNAME for a subdomain.
If Express needs the original client IP or protocol, configure trust proxy for your known proxy topology. Avoid enabling broad proxy trust without understanding how forwarded headers reach your application.
Operate, scale, and release safely
Automate releases and verify recovery
Move the infrastructure into AWS CDK, Terraform, or CloudFormation once the initial path works. Store infrastructure changes alongside application review workflows.
A GitHub Actions or AWS CodePipeline release should:
- Run tests and build a uniquely tagged image.
- Push it to ECR.
- Register a new task definition revision.
- Update the service and wait for stability.
- Perform an HTTPS smoke test.
Prefer workload federation, such as GitHub Actions OIDC, over stored AWS access keys. Roll back by updating the service to a known-good task definition revision.
Run database migrations as a controlled, one-off task. Use backward-compatible schema changes so old and new application versions can coexist during rolling deployments.
Monitor meaningful signals and control costs
Track ALB response times and 5xx errors alongside ECS CPU, memory, task restarts, and running-versus-desired task counts. Emit structured logs without credentials or sensitive request data.
Use target-tracking autoscaling based on tested CPU utilization or ALB requests per target. Load-test representative traffic before selecting thresholds and maximum capacity.
Budget for Fargate, ALB, networking, logs, image storage, secrets, and data transfer. NAT gateways and continuously running load balancers can materially affect a small service’s baseline. Consult AWS Fargate pricing and the AWS Pricing Calculator rather than relying on a single advertised compute price.
Common mistakes to avoid
- Wrong container architecture: build and task architecture must match.
- Unreachable application: listen on
0.0.0.0and check port mappings. - Failed image pulls: verify execution-role permissions and outbound networking.
- Unhealthy targets: confirm the health path, security groups, and startup grace period.
- Oversized permissions: separate infrastructure execution permissions from application access.
- State stored inside containers: replacements destroy local assumptions about files and sessions.
- Incomplete teardown: deleting an ECS service does not remove its ALB, NAT gateways, or retained logs.
Frequently asked questions
What is the easiest way to deploy a Node.js app on AWS?
Elastic Beanstalk can simplify deployment for conventional applications. ECS Fargate is a strong choice when you want container portability and explicit control over networking, releases, and scaling.
Can I deploy without Docker?
Yes. Elastic Beanstalk supports Node.js platform environments, while EC2 lets you install Node.js directly. Lambda can also deploy JavaScript packages without container images.
Is one Fargate task enough?
One task can be reasonable for development or a low-criticality service. Production applications needing availability should generally start with multiple tasks across Availability Zones and validate dependency resilience separately.
How do I update the app without downtime?
Use rolling ECS deployments with healthy overlapping capacity, graceful shutdown, and backward-compatible migrations. Validate the behavior under load; a deployment setting alone cannot guarantee uninterrupted requests.
For related cloud and delivery walkthroughs, browse more Tutorials topics.
Ask the community and get answers from practitioners.