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:
project/
├── redis/
│ └── docker-compose.yml
└── openresty/
└── docker-compose.yml
Below are the example configurations for each service.
Redis
version: '3'
services:
redis:
image: redis:latest
ports:
- "6379:6379"
OpenResty
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:
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:
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
docker0bridge - Port mappings such as
-p 6379:6379apply only to traffic between the host and the container, not between containers
Network Architecture Diagram
┌─────────────────────────────────────────────┐
│ 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.
- The request is scoped to the OpenResty container’s local network stack.
- Inside that namespace,
127.0.0.1resolves to the OpenResty container itself. - Redis is running in a different container, with a separate namespace.
- Therefore, no process is listening on port
6379inside 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:
ip route | grep default | awk '{print $3}'
Alternatively, you can check /etc/hosts:
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:
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.
Option 2: Use (Recommended for Development)
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.
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:
docker network create shared-network
Then configure both Docker Compose projects to use this shared network.
Redis docker-compose.yml:
version: '3'
services:
redis:
image: redis:latest
networks:
- shared-network
networks:
shared-network:
external: true
OpenResty docker-compose.yml:
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:
-- 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:
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:
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:
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:
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.
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:
- Use Kubernetes for built-in service discovery and orchestration.
- 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
-- 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:
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:
127.0.0.1inside a container points to itself, not the host- Port mapping operates on the host, not between containers
- Reliable inter-container communication methods include:
- The host gateway or
host.docker.internalfor host-level access. - A shared Docker network with service-based DNS resolution (recommended).
- The host gateway or
- 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.
