When running containers with Docker Compose, I sometimes want to access an unpublished port from a dependency container. For example, I sometimes need to connect directly to a database running inside a compose stack.
The usual solution is to edit (docker-)compose.yml and recreate the container with a published port. This works, but it is disruptive when the service is already running.
After experimenting a bit, I discovered a simple trick to expose container ports without recreating the containers. I'll document that in this post.
Docker makes it easy to run arbitrary processes inside an existing container by using commands like docker exec or docker compose exec.
This makes it possible to use a program like socat on the host to forward a local port into the container through standard input and output.
In practice, we create a TCP listener on the host, and whenever a connection comes in, socat spawns a second socat process inside the container, and the two processes communicate through STDIO.
The workflow looks like this:
Given the following compose.yml:
services:
my-stack:
image: alpine/socat
entrypoint: /bin/sh
init: true
command: ["-c", "sleep infinity"]
nginx:
image: nginx
network_mode: "service:my-stack"-
Install
socaton the host machine. -
Make sure
socatis available in the container. In the example above, I'm running nginx as a sidecar of the socat container to not deal with installing socat on the nginx container. Alternatively, you can installsocatusing the distro package manager in the nginx container, or you can copy in a static binary usingdocker cp. -
Start the relay:
HOST_PORT=8080 CONTAINER_PORT=80 SERVICE_NAME=my-stack socat TCP-LISTEN:$HOST_PORT,reuseaddr,fork \ EXEC:"docker 'compose exec $SERVICE_NAME socat STDIO TCP4:127.0.0.1:$CONTAINER_PORT'"
Here is the same approach in form of a convenient Bash script:
#!/usr/bin/env bash
# Usage: docker-expose <container-name> source-port:dest-port
# Example: docker-expose mysql 3306:3306
set -euo pipefail
if [ $# -ne 2 ]; then
echo "Usage: $(basename $0) <container-name> source-port:dest-port"
exit 1
fi
CONTAINER_NAME="$1"
PORTS="$2"
if [[ "$PORTS" != *:* ]]; then
echo "Error: port mapping must be in the form source:dest"
exit 1
fi
HOST_PORT="${PORTS%%:*}"
CONTAINER_PORT="${PORTS##*:}"
if ! command -v socat >/dev/null 2>&1; then
echo "Error: socat is not installed on the host. Please install it first."
exit 1
fi
# Note: This will not work for containers that are built from SCRATCH
if ! docker exec "$CONTAINER_NAME" sh -c "command -v socat" >/dev/null 2>&1; then
echo "Error: socat is not installed in the container '$CONTAINER_NAME'."
echo "Please install it or copy a static binary into the container."
exit 1
fi
echo "Forwarding host port $HOST_PORT to $CONTAINER_NAME:$CONTAINER_PORT"
echo "Press Ctrl+C to stop."
socat TCP-LISTEN:"$HOST_PORT",reuseaddr,fork \
EXEC:"docker 'exec -i $CONTAINER_NAME socat STDIO TCP4:127.0.0.1:$CONTAINER_PORT'"