NestJS + Docker on AWS ECS — Health Check Passing Locally, Failing in Production

NestJS Docker ECS health check failing in production while everything looks fine locally is one of the more disorienting bugs to debug, because every direct test you run says the app is healthy. docker run locally works. Exec into the running ECS task and curl the health endpoint yourself, and it responds instantly. ECS still marks the task unhealthy and cycles it every few minutes. The health check command itself is almost never the problem — it's a networking layer between wherever ECS runs the check from and your container that a manual curl from inside never has to cross.
Short answer: check your task's security group first. ECS (and separately, an ALB target group if you're using one) performs the health check from a specific network path, and if the security group doesn't allow inbound traffic on the health check port from that source, the check fails silently — no error in your app, nothing in your logs, because the request never arrives. Confirm the networking path before touching the health check command itself.

Why NestJS Health Checks Pass Locally but Fail on ECS
docker run and a real ECS task are answering fundamentally different questions. Locally, Docker's HEALTHCHECK instruction just needs your container's own network namespace to respond — there's no VPC, no security group, no separate networking layer to cross. On ECS, the health check (whether it's the task definition's own healthCheck or a separate ALB target group check) has to actually traverse whatever networking configuration sits between the checker and the container. A security group that only permits inbound traffic from certain sources will silently drop the exact request that's supposed to confirm the service is alive — and from your application's point of view, nothing happened. There's no failed request to log, because the request never got through.
(If you've spent an hour adjusting startPeriod and retry counts hoping a longer grace period fixes this — it won't, because the check isn't slow. It's not reaching the container at all. Timeout tuning fixes timing problems, not routing problems.)
Confirming It's Networking, Not the Check Itself
Before touching any configuration, isolate which layer is actually failing:
1# From your local machine, using ECS Exec to get a shell inside the running task
2aws ecs execute-command \
3 --cluster your-cluster \
4 --task your-task-id \
5 --container nestjs-app \
6 --interactive \
7 --command "/bin/sh"
8
9# Once inside, run the exact same command your health check uses
10curl -f http://localhost:3000/healthIf that curl succeeds from inside the task while ECS still reports it unhealthy, you've confirmed the health check command is fine and the problem is entirely in the networking path between the checker and the container. AWS's ECS Exec documentation covers the setup if you haven't enabled it on the cluster yet — it's worth having on regardless, since this exact diagnostic step comes up constantly.

Aligning the Health Check Across All Three Layers
There are potentially three separate health check configurations in play, and they need to agree:
1# Dockerfile — Docker's own container-level check
2HEALTHCHECK \
3 CMD curl -f http://localhost:3000/health || exit 1Docker's own HEALTHCHECK reference covers every option here in detail if you're tuning intervals for a slower-booting app.

1{
2 "containerDefinitions": [
3 {
4 "name": "nestjs-app",
5 "image": "your-account.dkr.ecr.region.amazonaws.com/nestjs-app:latest",
6 "healthCheck": {
7 "command": ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"],
8 "interval": 30,
9 "timeout": 5,
10 "retries": 3,
11 "startPeriod": 10
12 },
13 "portMappings": [
14 { "containerPort": 3000, "protocol": "tcp" }
15 ]
16 }
17 ]
18}Docker's HEALTHCHECK, the ECS task definition's own healthCheck block, and a separate ALB target group check (if you're behind a load balancer) are three independent configurations. ECS bases its restart decisions on its own check, not Docker's — a task can show healthy in docker inspect and still get cycled by ECS if the task definition's check, or the security group behind it, disagrees.
The Opinion Part
Here's the position worth stating plainly: running the same health check logic at three separate layers — Docker's HEALTHCHECK, the ECS task definition, and an ALB target group — isn't extra safety, it's three configuration surfaces that all have to agree before any of them do their actual job. In practice, most ECS health check failures come from two of those three layers quietly disagreeing, not from any single one being wrong. Pick one source of truth for what "healthy" means — the endpoint, the port, the timeout window — and reference the same values everywhere instead of redefining them three times and hoping they stay in sync as the app evolves.
Conclusion
If NestJS on Docker passes every health check you run yourself but ECS keeps marking the task unhealthy in production, stop adjusting timeouts and start checking the security group. Confirm the health check port is reachable from wherever ECS and your load balancer actually perform the check, verify the command from inside the task with ECS Exec, and make sure Docker's, ECS's, and the target group's health check configurations are all pointing at the same endpoint with compatible timing. Once the networking path is open, the health check that was "flaky" in production turns out to have been correct the entire time — it just never had a clear road to the container.
If this is the health check bug you're hitting after already solving it once on a different platform, our Render health check guide covers the equivalent failure mode there, and if you're still shaping the Docker image itself before it gets anywhere near ECS, our multi-stage build guide is worth reading first. Either way, once this is fixed, zero-downtime deploys on ECS stop being theoretical and start actually happening.
Fix the security group, watch the task stay green, and enjoy not getting paged for a health check that was never actually unhealthy.
Frequently Asked Questions
Because docker run locally and an ECS task running inside a VPC are answering different questions. Locally, Docker's HEALTHCHECK just needs your container's own network namespace to work. In ECS, the health check (and separately, the ALB target group check if you're using one) has to actually traverse the task's security group and networking configuration — and a security group that only allows inbound traffic from certain sources can quietly block the exact check that's supposed to confirm the service is healthy.
Docker's HEALTHCHECK instruction in the Dockerfile is a container-level check that Docker itself runs. The ECS task definition can define its own separate healthCheck block that ECS runs against the container. These are two independent configurations that need to agree — it's entirely possible for one to pass and the other to fail, and ECS bases its restart decisions on its own check, not Docker's.
Yes, and it's one of the most common causes. If the task's security group doesn't allow inbound traffic on the health check port from the source ECS or the load balancer uses to perform the check, the check will fail even though the exact same request succeeds when you exec into the container and run it from inside. The traffic never reaches the container in the first place.
Use ECS Exec to get a shell inside the running task and manually run the same command your health check uses. If it succeeds from inside but ECS still reports the task unhealthy, the problem is almost certainly in the networking path between the check's source and the container, not in the check command itself.
Yes — if you're running behind an Application Load Balancer, the target group has its own health check configuration, independent of both Docker's HEALTHCHECK and the ECS task definition's healthCheck block. All three can be configured differently, and a passing container-level check doesn't guarantee the target group agrees the task is healthy.
