Docker API server refuses connection from data pipeline

Problem Description

The data‑pipeline service, defined in a docker‑compose.yml file, attempts to call the API server at http://api:5000/api during ingestion. The call consistently fails with either a timeout or a connection‑refused error, causing the pipeline to abort.

Typical log excerpts:


pipeline   | 2026-06-12T08:14:32Z ERROR: dial tcp 172.18.0.3:5000: connect: connection refused
pipeline   | 2026-06-12T08:14:32Z ERROR: Get http://api:5000/api: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
api        | 2026-06-12T08:13:58Z WARN: listen tcp 127.0.0.1:5000: bind: address already in use

The failure manifests as:

  • Immediate connection refused when the API is reachable but bound only to 127.0.0.1.
  • Long‑running Client.Timeout exceeded when DNS resolution succeeds but the service is not listening on the expected port or network.

Root Cause Analysis

Docker’s networking model isolates containers on a user‑defined bridge network unless explicitly shared. Service discovery works via the embedded DNS server (127.0.0.11) that resolves the service name to its internal IP. The API container was:

  1. Listening only on 127.0.0.1 inside the container (as shown in the warning above). This prevents any other container from reaching the port because Docker’s bridge network routes traffic to the container’s external interface, not its loopback.
  2. Published with a mismatched port mapping (-p 8080:80 while the application actually listened on 5000), causing the pipeline to connect to the wrong host/port.
  3. Placed on a different network than the pipeline (default network vs. a custom network), leading to DNS lookup failures such as lookup api on 127.0.0.11:53: no such host.

These conditions line up with the community reports:

  • GitHub issue docker/compose#7619 – missing network alias and using localhost inside the client.
  • Stack Overflow 58712345 – solution was to bind the API to 0.0.0.0 and use the service name as host.
  • Docker Engine issue docker/engine#44733 – API bound to 127.0.0.1 caused external connection refusals.

Investigation and Debugging

Below is a reproducible debugging workflow that isolates the three typical failure vectors.

1. Verify container network topology

docker compose ps
docker network ls
docker network inspect mypipeline_default   # replace with actual network name

Expected output shows both api and pipeline attached to the same bridge network.

2. Test DNS resolution from the pipeline container

docker exec -it pipeline_container sh -c "getent hosts api"

If the command returns lookup api on 127.0.0.11:53: no such host, the services are on different networks or the service name is misspelled.

3. Check which address the API process is bound to

docker exec -it api_container sh -c "netstat -tlnp | grep 5000"
# or
docker exec -it api_container ss -tlnp | grep 5000

Typical problematic output:

tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      12/python

The address should be 0.0.0.0:5000 for cross‑container access.

4. Validate port publishing

docker compose config   # expands ports and networks

Look for mismatches such as 8080:80 when the container actually exposes 5000.

5. Quick connectivity test

docker exec -it pipeline_container curl -v http://api:5000/health

A Connection refused indicates binding or network isolation issues; a timed out points to port mapping or service readiness problems.

Resolution

The fix consists of three coordinated changes:

1. Bind the API server to 0.0.0.0

Update the application start‑up (example for a Flask app):

# Before
app.run(host='127.0.0.1', port=5000)

# After
app.run(host='0.0.0.0', port=5000)

2. Align Dockerfile EXPOSE and compose ports mapping

Dockerfile:

# Before
EXPOSE 80

# After
EXPOSE 5000

docker‑compose.yml (relevant excerpt):

# Before
services:
  api:
    image: myapi:latest
    ports:
      - "8080:80"
  pipeline:
    image: mypipeline:latest

# After
services:
  api:
    image: myapi:latest
    expose:
      - "5000"            # makes 5000 reachable on the internal network
    ports:
      - "5000:5000"      # optional host binding for external debugging
    networks:
      - backend
  pipeline:
    image: mypipeline:latest
    depends_on:
      - api
    networks:
      - backend

networks:
  backend:
    driver: bridge

3. Ensure both services share the same user‑defined bridge network

The backend network defined above guarantees DNS resolution and proper routing.

4. Add a healthcheck to delay dependent start‑up

api:
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
    interval: 5s
    timeout: 2s
    retries: 5

Verification

After applying the changes, run the stack and perform the following checks:

  1. Confirm both containers are on the same network:
    docker network inspect backend
    
  2. Validate DNS resolution from the pipeline:
    docker exec pipeline_container getent hosts api
    # Expected: 172.18.0.2 api
    
  3. Confirm the API is listening on 0.0.0.0:5000:
    docker exec api_container ss -tlnp | grep 5000
    # Expected: LISTEN 0.0.0.0:5000
    
  4. Run a direct curl request:
    docker exec pipeline_container curl -s -o /dev/null -w "%{http_code}" http://api:5000/health
    # Expected output: 200
    
  5. Observe pipeline logs for successful ingestion without connection errors.

Prevention and Best Practices

  • Always bind services to 0.0.0.0 unless there is a compelling reason to restrict to localhost. This ensures intra‑network reachability.
  • Declare expose ports in the Dockerfile and keep ports mappings in docker‑compose.yml consistent with the internal listening port.
  • Use a single user‑defined bridge network for all services that need to talk to each other. Docker’s documentation recommends this for reliable service discovery (Docker networking best practices).
  • Leverage depends_on together with healthchecks to avoid race conditions where the pipeline starts before the API is ready.
  • Monitor container logs for bind warnings such as listen tcp 127.0.0.1:5000: bind: address already in use, which often indicate a misconfiguration of the bind address.
  • Enable explicit network aliases if you need alternative hostnames, e.g., aliases: - api-service.

Related Topic Hub: Distributed Systems Troubleshooting Hub

FAQ

  1. Why does localhost work inside the API container but not from the pipeline?
    Because localhost resolves to the loopback interface of the calling container. The API is only listening on its own loopback, so other containers cannot reach it. Use the service name (e.g., api) and bind to 0.0.0.0.
  2. Can I expose the API only on the host without affecting container‑to‑container traffic?
    Yes. Publish the port with ports: "5000:5000" for host access, and keep expose: "5000" for internal traffic. Both coexist without conflict.
  3. What if my API container runs on a different port in production?
    Parameterize the port via an environment variable and reference it consistently in EXPOSE, ports, and the application bind call. This avoids hard‑coded mismatches.
  4. How do I debug DNS resolution failures inside a container?
    Use getent hosts or dig @127.0.0.11 . Failure usually indicates the services are on different Docker networks or the service name is misspelled.
  5. Is the host network mode ever appropriate for a data‑pipeline container?
    Only when the pipeline must reach services on the host network directly. Mixing host mode with bridge‑mode services breaks Docker’s internal routing and leads to intermittent connection errors, as seen in the real incident list.