Why You Can’t Access Local Redis via 127.0.0.1 in a Docker Environment

20 Oct 20255 minute
Junwen Feng

Junwen Feng

Engineer

docker network

Scenario Description

At InsForge, our cloud backend is built on OpenResty as a reverse proxy, paired with a Redis Cluster for service discovery. This setup lets us dynamically route developer API requests based on real-time project information.

Recently, we wanted to replicate this setup in a smaller environment for local experiments. To keep things simple, Redis and OpenResty each run in their own Docker Compose project: one serving as the storage backend and the other acting as the reverse proxy.

The project structure looks like this:

text
project/
├── redis/
│   └── docker-compose.yml
└── openresty/
    └── docker-compose.yml

Below are the example configurations for each service.

Redis

yaml
version: '3'
services:
  redis:
    image: redis:latest
    ports:
      - "6379:6379"

OpenResty

yaml
version: '3'
services:
  openresty:
    image: openresty/openresty:latest
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf

The Problem: 127.0.0.1 Cannot Connect to Redis

In OpenResty’s Lua code, we attempt to connect to Redis:

lua
local redis = require "resty.redis"
local red = redis:new()

-- Try to connect to local Redis
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.log(ngx.ERR, "failed to connect to redis: ", err)
    return
end

But when this code runs, the logs show an unexpected error:

text
failed to connect to redis: connection refused

This is unexpected. Redis has already been mapped to the host port 6379 using -p 6379:6379, which makes it accessible from the host machine. However, when accessed from within another container, the connection fails.

Understanding Docker Networking

The Principle of Docker Network Isolation

To understand this behavior, we need to look at how Docker manages container networking. Every container runs in its own virtual network stack, which provides:

  • Its own network interfaces
  • Its own IP address
  • Its own routing table
  • Its own port space

When a container starts, Docker automatically creates this virtual network namespace. Within this environment:

  • 127.0.0.1 (localhost) refers only to the container itself, not the host
  • Each container receives a private IP address, typically within the subnet of the docker0 bridge
  • Port mappings such as -p 6379:6379 apply only to traffic between the host and the container, not between containers

Network Architecture Diagram

text
┌─────────────────────────────────────────────┐
│                Host Machine                 │
│                                             │
│  127.0.0.1:6379 ← Redis port mapping        │
│         ↓                                   │
│  ┌──────────────────-┐  ┌─────────────────┐ │
│  │  Redis Container  │  │ OpenResty Cont. │ │
│  │ 127.0.0.1 → self  │  │ 127.0.0.1 → self│ │
│  │ IP: 172.17.0.2    │  │ IP: 172.18.0.2  │ │
│  └───────────────────┘  └─────────────────┘ │
│           ↑                      ↑          │
│           └──────────┬───────────┘          │
│                Docker Network Layer         │
└─────────────────────────────────────────────┘

Why Doesn’t 127.0.0.1 Work?

When the OpenResty container attempts to connect to 127.0.0.1:6379, the connection is handled entirely within its own namespace.

  1. The request is scoped to the OpenResty container’s local network stack.
  2. Inside that namespace, 127.0.0.1 resolves to the OpenResty container itself.
  3. Redis is running in a different container, with a separate namespace.
  4. Therefore, no process is listening on port 6379 inside OpenResty, resulting in a “connection refused” error.

Although Redis’s port is mapped to the host, this mapping is not visible inside other containers. Each container is isolated by design, so they cannot reach each other through 127.0.0.1 unless explicitly connected to a shared network.

Correct Connection Methods

There are several ways to connect containers so that OpenResty can successfully reach Redis. Each approach depends on your environment and use case.

Option 1: Use the Host Gateway IP

Each Docker network defines a gateway IP, usually the first address in the subnet. For the default bridge network, this is often 172.17.0.1.

To find the gateway IP inside the container:

bash
ip route | grep default | awk '{print $3}'

Alternatively, you can check /etc/hosts:

bash
cat /etc/hosts
# You may see:
# 172.18.0.1    host.docker.internal

Once you have the gateway IP, update the Lua connection code as follows:

lua
local redis = require "resty.redis"
local red = redis:new()

local ok, err = red:connect("172.18.0.1", 6379)
if not ok then
    ngx.log(ngx.ERR, "failed to connect to redis: ", err)
    return
end

The gateway IP acts as a bridge between the container and the host. Containers can use this address to access host-exposed ports, such as Redis mapped with -p 6379:6379.

Docker Desktop provides a special DNS name host.docker.internal, which resolves to the host IP. This allows containers to communicate with services exposed on the host.

lua
local ok, err = red:connect("host.docker.internal", 6379)

This approach works well for local development and testing because it requires no additional network setup.

Notes:

  • Works by default on Docker Desktop for Mac/Windows
  • On Linux, it must be added manually by including a host entry when starting the container: --add-host=host.docker.internal:host-gateway

This method provides a simple way to reach host-exposed ports without modifying Docker Compose networks. However, it is generally best suited for local development, not production environments.

Option 3: Use a Shared Docker Network (Best Practice)

For long-term maintainability and service-to-service communication, the recommended approach is to connect containers through a shared Docker network.

Start by creating an external network:

bash
docker network create shared-network

Then configure both Docker Compose projects to use this shared network.

Redis docker-compose.yml:

yaml
version: '3'
services:
  redis:
    image: redis:latest
    networks:
      - shared-network

networks:
  shared-network:
    external: true

OpenResty docker-compose.yml:

yaml
version: '3'
services:
  openresty:
    image: openresty/openresty:latest
    ports:
      - "80:80"
    networks:
      - shared-network

networks:
  shared-network:
    external: true

Once both containers are connected to the same network, Docker’s built-in DNS automatically resolves service names. The OpenResty container can connect to Redis using its service name.

Then in Lua:

lua
-- Use the service name directly; Docker DNS will resolve it
local ok, err = red:connect("redis", 6379)

Using a shared network keeps configuration clean, avoids host-specific dependencies, and ensures reliable communication across environments. This is considered the best practice for production deployments.

How Docker Compose Service Name Resolution Works

When multiple services are defined in the same docker-compose.yml:

yaml
version: '3'
services:
  redis:
    image: redis:latest
  
  openresty:
    image: openresty/openresty:latest

The OpenResty container can connect using redis as the hostname because:

1. Docker’s Built-in DNS Server

Each user-defined Docker network includes an internal DNS server, typically located at 127.0.0.11. This server maintains the mapping between service names and container IP addresses.

You can confirm this by checking the container’s resolver configuration:

bash
cat /etc/resolv.conf
# nameserver 127.0.0.11

2. Service Discovery

Docker Compose automatically performs the following for each service:

  • Creates a DNS record for each service name
  • Resolves the name to the container IP
  • Supports round-robin DNS for scaled services

Example:

bash
nslookup redis
# Name: redis
# Address: 172.18.0.2

This means any container in the same Docker network can connect to Redis simply by referencing redis as the hostname.

3. Network Aliases

Docker also supports network aliases, allowing multiple hostnames to resolve to the same container. Example configuration:

yaml
version: '3'
services:
  redis:
    image: redis:latest
    networks:
      app-network:
        aliases:
          - redis-master
          - cache-server

networks:
  app-network:

In this configuration, the names redis, redis-master, and cache-server all resolve to the same Redis container. This can be helpful when multiple hostnames are required for compatibility or migration.

4. Cross-Compose Service Discovery

By default, services defined in different Docker Compose projects cannot communicate with each other because each project creates its own isolated network. To enable cross-project communication, you can use one of the following approaches:

  • Connect both projects to a shared external network (see Option 3)
  • Use the host gateway or port mappings (see Options 1 and 2)

Once both services are attached to the same network, Docker’s internal DNS will resolve all service names within that network, allowing seamless communication between containers across projects.

Best Practice Recommendations

For Development

For local development, the simplest and most reliable approach is to connect all containers through a shared Docker network.

bash
docker network create dev-network

Once the network is created, attach each service to it through its respective Compose file. Containers on the same network can communicate directly using service names, without relying on IP addresses or host mappings.

For Production

In production environments, network design and service discovery should be more structured. Consider the following approaches:

  1. Use Kubernetes for built-in service discovery and orchestration.
  2. Deploy a Service Mesh such as Istio or Linkerd to manage advanced routing, observability, and security. 3.Integrate dedicated discovery tools like Consul or Etcd for fine-grained control in distributed systems.

These solutions provide better scalability, reliability, and visibility than manual container networking.

Configuration Management

To make the setup more flexible, externalize the Redis connection configuration instead of hardcoding host and port values.

config.lua

lua
-- config.lua
local _M = {}
_M.redis_host = os.getenv("REDIS_HOST") or "host.docker.internal"
_M.redis_port = tonumber(os.getenv("REDIS_PORT")) or 6379
return _M

In your docker-compose.yml, set the environment variables so each service can load its configuration dynamically:

yaml
services:
  openresty:
    image: openresty/openresty:latest
    environment:
      - REDIS_HOST=redis
      - REDIS_PORT=6379

Summary

Docker network isolation is powered by Linux namespaces, giving each container its own network stack. Key takeaways:

  1. 127.0.0.1 inside a container points to itself, not the host
  2. Port mapping operates on the host, not between containers
  3. Reliable inter-container communication methods include:
    • The host gateway or host.docker.internal for host-level access.
    • A shared Docker network with service-based DNS resolution (recommended).
  4. Docker Compose uses an internal DNS service to automatically resolve service names to container IPs.

Understanding these principles helps prevent connectivity issues and leads to cleaner, more maintainable microservice architectures.