If you’ve ever needed to show a client your local development environment or test a webhook integration, you know the pain of trying to expose a local service to the Internet. There are tools like ngrok and serveo that help, but what if you want full control of the tunneling server? Or what if you just want a dead-simple Docker-based setup that you can drop into any existing project?
That’s why I built Topo. The name means “mole” in Spanish (because it digs tunnels). Get it?
The Problem
During development, there are plenty of situations where you need your local machine to be accessible from the outside world:
- Showing progress to a client without deploying to a staging server
- Testing webhooks from third-party services (payment gateways, messaging platforms, etc.)
- Sharing your local dev environment with a colleague for quick debugging
- Testing how your application behaves behind a real domain with SSL
The usual approach involves either paying for a tunneling service, using a free tier with limitations, or setting up complex SSH configurations manually. I wanted something that was self-hostable, containerized, and easy to plug into existing Docker-based projects.
What It Does
Topo is a Dockerized reverse SSH tunneling solution with two components:
- Server (
roura/topo:server) - A self-hosted serveo instance that you run on your own domain. You control the tunnel endpoint. - Client (
roura/topo:client) - A lightweight Alpine-based container that establishes the tunnel. It’s designed to be added to any existingdocker-compose.ymlfile alongside your application.
The idea is simple: you host the server on a machine with a public IP and a domain, then use the client container locally to tunnel traffic from that domain straight to your local service.
How It Works
The client container runs a small shell script that establishes a reverse SSH tunnel:
ssh \
-R $NAME.$SERVICE:$REMOTE_PORT:$CONTAINER:$HOST_PORT \
-p $SERVICE_PORT \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o StrictHostKeyChecking=no \
-o TCPKeepAlive=yes \
$SERVICE
It connects to the server, tells it to forward traffic from $NAME.$SERVICE (e.g., topo.yourdomain.com) back to a local container on your Docker network. The ServerAliveInterval and TCPKeepAlive options ensure the tunnel stays alive even through network hiccups.
Getting Started
Using the Client
The quickest way to use Topo is by adding the client to your existing docker-compose.yml. Here’s an example that tunnels a PHP/Apache container:
version: '3.6'
services:
httptunnel:
image: roura/topo:client
network_mode: bridge
container_name: httptunnel
tty: true
stdin_open: true
command: sh connect
links:
- web
web:
image: php:apache
network_mode: bridge
container_name: website
The tunnel container links to your web service and forwards traffic to it. You can replace php:apache with whatever your application image is.
Hosting Your Own Server
If you want full control, you can self-host the server component on any machine with a public IP:
version: '3.6'
services:
proxy:
build:
context: .
network_mode: bridge
container_name: tunnelserver
command: sh start
restart: on-failure
ports:
- 80:80
- 443:443
environment:
SERVICE: your.domain-name.here
Point your domain’s DNS to the server, deploy the container, and you’ve got your own tunneling infrastructure. No third-party dependencies, no usage limits.
Configuration
The client is configured through environment variables:
| Variable | Description | Default |
|---|---|---|
SERVICE_PORT |
SSH port on the tunnel server | 443 |
HOST_PORT |
Port of your local service | 80 |
REMOTE_PORT |
Port to expose on the server | 80 |
CONTAINER |
Name of the linked container | web |
NAME |
Subdomain prefix | topo |
SERVICE |
Tunnel server domain | serveo.net |
By default, Topo uses serveo.net as the tunneling service, so you can use it out of the box without hosting your own server. When you’re ready for full control, just change the SERVICE variable to your own domain.
Why Docker?
Wrapping the tunnel in a Docker container has a few practical advantages:
- No local dependencies - You don’t need SSH configured on your host machine
- Composable - Drop it into any
docker-compose.ymlalongside your existing services - Isolated - The tunnel runs in its own container, separate from your application
- Portable - Works the same on any OS that runs Docker
Limitations
- The tunnel relies on SSH, so it’s subject to network conditions and SSH connection limits
- The self-hosted server requires a machine with a public IP and a domain
- Not designed for high-traffic production use - this is a development tool
- The server component bundles a
serveobinary, which hasn’t been updated in a while
Try It Out
The project is available at github.com/rouralberto/topo. If you’re already using Docker for local development and need a quick way to expose services to the Internet, just add the Topo client to your compose file and you’re set. And if you want to go all-in, host your own server and own the entire pipeline.