A tool to create SSH tunnels to Docker Compose containers with automatic Caddy reverse proxy setup, using *.localhost domains.
Inspired by/built upon kobemertens/worm. This was also a vibe code experimemt. Issues/pr's welcome.
- 🔌 SSH tunnels to remote Docker containers
- 🌐 Automatic domain aliases using
*.localhost(no/etc/hostsediting needed) - 🔄 Caddy reverse proxy for clean HTTP access
- 🎯 Interactive service selection with fzf
- 📦 Works with Docker Compose projects
- 🚀 Discovers projects automatically from running containers
Install dependencies:
# Ubuntu/Debian
sudo apt install fzf openssh-client docker.io
# macOS
brew install fzf openssh dockerMake scripts executable:
chmod +x tunnel-app.sh container-tunnel-caddy list-tunnelsCaddy starts automatically the first time you open a tunnel. Don't run docker compose up -d directly — the Caddy container bind-mounts ~/.cache/worm/sockets/, and if Docker creates that directory first it will be owned by root and the script won't be able to write sockets into it. Always use ./tunnel-app.sh (which creates the directory first).
./tunnel-app.shShows an fzf menu to select from all available Docker Compose projects across all SSH hosts.
./tunnel-app.sh contactgegevensAuto-selects if only one match, otherwise shows filtered fzf menu.
./tunnel-app.sh -o- Select a project
- Select a service from that project
- Creates tunnels with naming:
<service>.<project>.<host>.localhost
-o, --other: Choose a specific service interactively-v, --verbose: Enable verbose logging-r PORT: Specify remote port (default: 8890)--refresh: Refresh project cache-h: Show help
./tunnel-app.sh my-app
# Creates: http://my-app.dev.example.com.localhost/sparql./tunnel-app.sh -o my-app
# Select "frontend" from menu
# Creates: http://frontend.my-app.dev.example.com.localhost./tunnel-app.sh -v -o
# Shows debug information including SSH commands- Discovery: Queries Docker on remote hosts to find running Compose projects
- Caching: Caches project lists for performance (use
--refreshto update) - Tunneling: Opens
ssh -L <unix-socket>:<container>:<port>per service — each tunnel binds to a Unix domain socket under~/.cache/worm/sockets/, not a TCP port. Nothing onlocalhost:<port>is reachable, so the only way in is through Caddy. - Reverse Proxy: Generates a
Caddyfilewith one<domain>:80 { reverse_proxy unix//sockets/<domain>.sock }block per tunnel and hot-reloads Caddy via its admin API. - Domain Resolution: Uses
*.localhostwhich automatically resolves to 127.0.0.1.
- Default mode:
<project>.<ssh-host>.localhost - Other service mode:
<service>.<project>.<ssh-host>.localhost - Virtuoso services automatically get
/sparqlsuffix
All URLs are served on port 80 by Caddy — no port suffix needed.
- SSH access to remote hosts (configured in
~/.ssh/config) - Docker running on remote hosts with Compose projects
- Local Docker for Caddy reverse proxy
- Port 80 available locally (for Caddy)
./tunnel-app.sh --refreshCheck if Caddy is running:
docker ps | grep worm-caddyEnsure your SSH keys are properly configured in ~/.ssh/config
tunnel-app.sh: Main entry point, handles project selectioncontainer-tunnel-caddy: Core tunneling logic with Caddy integrationlist-tunnels: Show active tunnels and prune stale statedocker-compose.yml: Caddy reverse proxy configuration (base)docker-compose.macos.yml: macOS overlay (bridge network + port mappings)
MIT