Exposing Ports of a Running Container

Published on Dec 2, 2025
In Gistful Musings
Comments and Reactions

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.

Mapping Ports Through Standard Streams

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"
  1. Install socat on the host machine.

  2. Make sure socat is 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 install socat using the distro package manager in the nginx container, or you can copy in a static binary using docker cp.

  3. 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'"
    

Resuable Script

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'"