Bridge Docker containers to host localhost services via socat. Solves the #1 networking issue in containerized AI agent deployments: containers can't reach s...
---
name: localhost-bridge
description: "Bridge Docker containers to host localhost services via socat. Solves the #1 networking issue in containerized AI agent deployments: containers can't reach services bound to 127.0.0.1."
homepage: "https://casys.ai/blog/the-localhost-trap"
source: "https://github.com/Casys-AI/casys-pml-cloud"
author: "Erwan Lee Pesle (superWorldSavior)"
always: false
privileged: false
requires:
- sudo access (systemd service creation, UFW rules)
- Docker daemon access (docker inspect, docker network inspect)
- socat package (apt install socat)
---
# localhost-bridge — Connect containers to host localhost services
## ⚠️ Security & Privileges
**This skill requires host-level privileges.** It must be reviewed and executed manually by an administrator — never autonomously by an agent.
What it does on the host:
- Creates a **systemd service** (persistent across reboots) that forwards traffic from a Docker bridge IP to localhost
- Adds a **UFW firewall rule** scoped to a specific Docker bridge interface
- Requires **sudo**, **Docker daemon access**, and **socat** from your distro's official package repository
**Before running any command:**
1. Review the generated `/etc/systemd/system/socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT>.service` file — confirm `ExecStart` binds only to the intended Docker bridge IP (172.x.x.1), never 0.0.0.0
2. Review the UFW rule — confirm it targets the correct `br-<ID>` interface and port
3. After setup, verify the port is NOT reachable from the public network: `curl --connect-timeout 2 http://<PUBLIC_IP>:<PORT>/` must fail
4. Test from inside a container before deploying widely
**Do not grant an automated agent permissions to run these commands without human approval.**
---
## The Problem
A service on the host listens on `127.0.0.1` (AI gateway, MCP server, Ollama, database...). A Docker container needs to reach it. `localhost` inside the container points to the container itself, not the host. Requests either timeout silently (firewall drops packets) or get connection refused.
## The Solution
`socat` listens on the Docker bridge gateway IP and forwards to host loopback. Combined with a scoped firewall rule, this gives containers access without exposing the service externally.
## Setup (run manually as admin)
### 1. Find the Docker bridge gateway IP
```bash
# For a specific container
docker inspect <container_name> --format '{{json .NetworkSettings.Networks}}' \
| python3 -c "
import json,sys
d = json.load(sys.stdin)
for net, info in d.items():
print(f'{net}: gateway={info[\"Gateway\"]}')"
```
### 2. Create a systemd service
Replace `<GATEWAY_IP>`, `<PORT>`, `<SOURCE_NETWORK>`, and `<TARGET_SERVICE>` with your values.
**Naming convention:** `socat-<source_network>-<target_service>-<port>` — source network is the Docker network (consumer), target service is the host service. Self-documenting.
Examples: `socat-bridge-gateway-18789`, `socat-windmill_default-gateway-18789`, `socat-bridge-ollama-11434`
**Review the ExecStart line before enabling** — confirm it binds to the Docker bridge IP only.
```bash
sudo tee /etc/systemd/system/socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT>.service > /dev/null << 'EOF'
[Unit]
Description=Socat bridge: <SOURCE_NETWORK> -> <TARGET_SERVICE>:<PORT>
After=network.target docker.service
[Service]
Type=simple
ExecStart=/usr/bin/socat TCP-LISTEN:<PORT>,bind=<GATEWAY_IP>,fork,reuseaddr TCP:127.0.0.1:<PORT>
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# Review the file before enabling:
cat /etc/systemd/system/socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT>.service
sudo systemctl daemon-reload
sudo systemctl enable --now socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT>
```
### 3. Add firewall rule (MANDATORY)
Without this, socat listens but packets from the container are silently dropped — causing 30-second timeouts with no error.
**Review the bridge ID before applying** — a wrong ID can expose services.
```bash
# Find the Linux bridge interface for the Docker network
BRIDGE_ID=$(docker network inspect <network_name> --format '{{.Id}}' | cut -c1-12)
# Verify this is the right bridge
ip link show br-${BRIDGE_ID}
# Allow traffic only on that bridge interface
sudo ufw allow in on br-${BRIDGE_ID} to any port <PORT> proto tcp comment "<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT>"
```
### 4. Verify security
```bash
# MUST succeed (from inside a container)
docker exec <container_name> curl -s --connect-timeout 5 http://<GATEWAY_IP>:<PORT>/
# MUST fail (from the public network)
curl --connect-timeout 2 http://<PUBLIC_IP>:<PORT>/
```
## Multi-Network Workers
A container can be on multiple Docker networks. Each has its own bridge IP. You need a socat instance + firewall rule for **each network** the container uses. In practice, one network is usually enough.
Check all networks: `docker inspect <container> --format '{{json .NetworkSettings.Networks}}'`
## Common Use Cases
| Host service | Container client | Default port |
|---|---|---|
| AI gateway (OpenClaw, LiteLLM) | Workflow orchestrator (Windmill, n8n) | 18789 |
| MCP server | Dockerized agent | varies |
| Ollama | RAG pipeline, agent | 11434 |
| PostgreSQL | API server | 5432 |
| Redis | Any containerized app | 6379 |
## Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 30s timeout, no error | Firewall dropping packets | Add UFW rule on the bridge interface |
| Connection refused | socat not running | `systemctl status socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT>` |
| Works then stops after Docker restart | Bridge IP changed | Check new gateway IP, update socat bind |
| socat won't start after reboot | Docker not ready | Ensure `After=docker.service` in unit file |
## Alternatives
Depending on your security posture, consider:
- **Docker host networking** (`network_mode: host`) — simpler but removes all container network isolation
- **Running socat inside a minimal privileged container** — avoids host-level systemd changes
- **Configuring the host service to bind to the Docker bridge IP directly** — no socat needed, but the service must support custom bind addresses
- **`host.docker.internal`** (Docker Desktop) — works on Mac/Windows, not reliably on Linux
## Prerequisites
Install socat from your distro's official package repository:
```bash
sudo apt-get install -y socat # Debian/Ubuntu
sudo dnf install -y socat # Fedora/RHEL
```
## References
- Blog post: [The Localhost Trap](https://casys.ai/blog/the-localhost-trap) — why this problem exists and why it matters for AI infrastructure
- Source: [Casys-AI/casys-pml-cloud](https://github.com/Casys-AI/casys-pml-cloud)
- Docker docs: [Packet filtering and firewalls](https://docs.docker.com/engine/network/packet-filtering-firewalls/)
don't have the plugin yet? install it then click "run inline in claude" again.
structured original procedural steps into explicit numbered procedure, added edge cases for docker restart, bridge IP reassignment, firewall alternatives, multi-network containers, and added detailed decision points for UFW vs firewalld vs iptables, missing firewall rules, and docker bridge IP changes.
this skill bridges Docker containers to host localhost services using socat, a TCP forwarding utility. it solves the core networking problem in containerized deployments: containers can't reach services bound to 127.0.0.1 on the host because localhost inside a container routes to the container itself, not the host. use this when your containerized agents, orchestrators, or pipeline components need to talk to host-local services like AI gateways, MCP servers, Ollama, databases, or caches without exposing those services to the public network.
docker inspect, docker network inspect, and docker exec commands. typically requires user to be in the docker group or have sudo docker access.sudo apt-get install -y socat (Debian/Ubuntu) or sudo dnf install -y socat (Fedora/RHEL). must be in /usr/bin/socat.sudo ufw allow. if firewalld or iptables is used instead, adjust firewall commands accordingly.systemctl list-units --type=service | grep socat first.Identify the Docker bridge gateway IP for your network.
my-agent), Docker network name (e.g., bridge, windmill_default).docker inspect <container_name> --format '{{json .NetworkSettings.Networks}}' | python3 -c "import json,sys; d = json.load(sys.stdin); [print(f'{net}: gateway={info[\"Gateway\"]}') for net, info in d.items()]"bridge: gateway=172.17.0.1.docker inspect will still work but NetworkSettings may show stale data. restart the container to refresh.Review the socat configuration before deploying.
socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT> (e.g., socat-bridge-ollama-11434).ExecStart=/usr/bin/socat TCP-LISTEN:<PORT>,bind=<GATEWAY_IP>,fork,reuseaddr TCP:127.0.0.1:<PORT>.Create and enable the systemd service.
sudo tee /etc/systemd/system/socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT>.service > /dev/null << 'EOF' <unit_file_content> EOFsudo systemctl daemon-reloadsudo systemctl enable --now socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT>sudo systemctl status socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT> (should show "active (running)").After=docker.service. wait for Docker to be ready and manually restart socat via sudo systemctl restart socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT>.Identify the Linux bridge interface for the Docker network.
docker network inspect <network_name> --format '{{.Id}}' | cut -c1-12a1b2c3d4e5f6).br-<ID> (e.g., br-a1b2c3d4e5f6).ip link show br-<ID> to confirm the bridge exists. if not found, the network may not have created a bridge (rare; indicates a misconfigured network).Add UFW firewall rule scoped to the bridge interface.
br-a1b2c3d4e5f6) from step 4, port number, source network name, target service name.sudo ufw status. if inactive, skip this step.sudo ufw allow in on br-<ID> to any port <PORT> proto tcp comment "<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT>"sudo ufw show added or sudo ufw status numbered | grep <PORT>.sudo firewall-cmd --add-rich-rule 'rule family="ipv4" source address="172.17.0.0/16" inbound port protocol="tcp" port="<PORT>" accept').Test from inside the container.
docker exec <container_name> curl -s --connect-timeout 5 http://<GATEWAY_IP>:<PORT>/200 OK, service-specific output). if timeout or connection refused, socat is not running or firewall rule is missing.sudo systemctl status socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT> for socat errors. check sudo ufw status | grep <PORT> to confirm rule is present.Verify the service is not reachable from the public network.
curl --connect-timeout 2 http://<PUBLIC_IP>:<PORT>/If container is on multiple Docker networks: you need a separate socat service + UFW rule for each network. check all networks via docker inspect <container> --format '{{json .NetworkSettings.Networks}}'. if the container has 2 networks (e.g., bridge and custom), create two separate socat services (e.g., socat-bridge-ollama-11434 and socat-custom-ollama-11434).
If UFW is not installed or disabled: skip step 5 entirely. socat will still work, but will be accessible to any process on the host with network access to the bridge interface (less isolated, but acceptable in trusted environments).
If using firewalld or iptables instead of UFW: adapt the firewall command in step 5. for firewalld, use sudo firewall-cmd --permanent --add-rich-rule 'rule family="ipv4" source address="<BRIDGE_NETWORK>" inbound port protocol="tcp" port="<PORT>" accept'. for iptables, use sudo iptables -A DOCKER-USER -i br-<ID> -p tcp --dport <PORT> -j ACCEPT.
If Docker bridge IP changes after container restart: socat will no longer work because it binds to a stale IP. docker can reassign bridge IPs if the container is recreated or if docker daemon restarts. either (a) use --ip flag when starting the container to pin the IP, or (b) automate bridge IP discovery by running step 1 periodically and restarting socat if the IP changes.
If test in step 6 times out: firewall rule is missing or UFW silently dropped the rule (e.g., due to interface name mismatch). first verify the bridge interface name with ip link show br-<ID>. then verify the rule was added with sudo ufw show added. if the interface name is wrong, delete the rule with sudo ufw delete allow in on br-<WRONG_ID> to any port <PORT> proto tcp and re-run step 5 with the correct interface.
If socat fails to start after Docker restart: the After=docker.service dependency may not be enough if docker takes time to initialize the bridge. manually restart socat via sudo systemctl restart socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT> or increase the restart delay by adding StartLimitInterval=30s and StartLimitBurst=5 to the [Service] section.
systemd service file: created at /etc/systemd/system/socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT>.service. file must be readable via cat /etc/systemd/system/socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT>.service and must contain ExecStart=/usr/bin/socat TCP-LISTEN:<PORT>,bind=<GATEWAY_IP>,fork,reuseaddr TCP:127.0.0.1:<PORT>.
systemd service enabled: service must appear in systemctl list-units --type=service | grep socat and must show enabled via systemctl is-enabled socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT>.
systemd service running: sudo systemctl status socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT> must show active (running) and PID must be present.
UFW rule added (if UFW enabled): rule must appear in sudo ufw status numbered with comment matching <SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT> and must specify the correct bridge interface (e.g., in on br-a1b2c3d4e5f6).
socat listening on bridge IP: verify via sudo netstat -tlnp | grep socat or sudo ss -tlnp | grep socat. output must show socat listening on <GATEWAY_IP>:<PORT> (e.g., 172.17.0.1:11434), NOT 0.0.0.0:<PORT>.
From inside the container: docker exec <container_name> curl -s --connect-timeout 5 http://<GATEWAY_IP>:<PORT>/ returns a 200 or 2xx status code and service response (e.g., Ollama returns JSON, PostgreSQL rejects HTTP but tcp connection succeeds, etc.). no timeout. no connection refused.
From the host: curl -s --connect-timeout 5 http://127.0.0.1:<PORT>/ works (if service is up), confirming socat is proxying correctly.
From the public network: curl --connect-timeout 2 http://<PUBLIC_IP>:<PORT>/ times out or returns connection refused, confirming the service is not exposed externally.
systemd logs: sudo journalctl -u socat-<SOURCE_NETWORK>-<TARGET_SERVICE>-<PORT> -n 20 shows no error messages (or only initial startup messages like "listening on
Docker container can now reach the host service: application logs inside the container show successful connections to the host service (e.g., AI agent connecting to Ollama, workflow orchestrator connecting to AI gateway, pipeline connecting to PostgreSQL).
credits: skill authored by Erwan Lee Pesle (superWorldSavior). original blog post and source at casys.ai.