Docker Integration
Expose services running inside Docker containers through NullBore tunnels — without publishing ports to your host machine.
Why a separate container?
NullBore runs as a sidecar container on the same Docker network as your services. It connects outbound to the NullBore relay server (port 443) and forwards inbound tunnel traffic to your containers by hostname.
This means:
- Your services don't need published ports (
-pflags) - NullBore resolves container names via Docker's internal DNS
- Each project can have its own API key and tunnel config
- No changes to your existing containers
Quick start
1. Pull the image
docker pull ghcr.io/nullbore/tunnel:latest
The image is published for linux/amd64 and linux/arm64 (Raspberry Pi, Apple Silicon).
2. Create a dedicated API key
Go to your NullBore dashboard and create a new API key specifically for this Docker project. Using a separate key per project means:
- You can revoke access to one project without affecting others
- Device binding tracks each project independently
- Tunnel configs are scoped to the key
- Bandwidth usage is attributable
3. Add NullBore to your Compose file
services:
# Your existing service
webapp:
image: node:20-alpine
working_dir: /app
volumes:
- ./:/app
command: npm start
# No ports needed — NullBore handles external access
# NullBore sidecar
tunnel:
image: ghcr.io/nullbore/tunnel:latest
environment:
- NULLBORE_API_KEY=nbk_your_key_here
- NULLBORE_SERVER=https://tunnel.nullbore.com
- NULLBORE_TUNNELS=webapp:3000:my-webapp
depends_on:
- webapp
restart: unless-stopped
That's it. Run docker compose up and NullBore will expose webapp:3000 at https://my-webapp.yoursubdomain.nullbore.com.
Configuration
Environment variables
| Variable | Required | Description |
|---|---|---|
NULLBORE_API_KEY | ✅ | Your API key (from dashboard) |
NULLBORE_SERVER | No | Relay server URL (default: https://tunnel.nullbore.com) |
NULLBORE_TUNNELS | No | Static tunnel definitions (see below) |
Tunnel format
NULLBORE_TUNNELS accepts a comma-separated list of tunnels:
host:port:name,host:port:name,...
- host — container hostname (the service name in Compose)
- port — the port inside the container
- name — tunnel name (becomes the subdomain slug, requires Dev+ with a claimed subdomain)
Examples:
# Single service
NULLBORE_TUNNELS=webapp:3000:web
# Multiple services
NULLBORE_TUNNELS=api:8080:api,frontend:3000:app,postgres-admin:8081:db-admin
# Without names (random slugs, works on free tier)
NULLBORE_TUNNELS=webapp:3000,api:8080
Config file mode
For more control (TTL, auth, idle timeout), mount a config file:
services:
tunnel:
image: ghcr.io/nullbore/tunnel:latest
environment:
- NULLBORE_API_KEY=nbk_your_key_here
volumes:
- ./nullbore.toml:/home/nullbore/.config/nullbore/config.toml:ro
depends_on:
- webapp
# nullbore.toml
server = "https://tunnel.nullbore.com"
[[tunnels]]
name = "api"
host = "webapp"
port = 3000
ttl = "8h"
auth = "demo:s3cret"
[[tunnels]]
name = "docs"
host = "docs-server"
port = 4000
Dashboard-driven mode
If you omit NULLBORE_TUNNELS, the container runs in daemon mode and manages tunnels configured in your NullBore dashboard:
services:
tunnel:
image: ghcr.io/nullbore/tunnel:latest
environment:
- NULLBORE_API_KEY=nbk_your_key_here
Full example: multi-service project
# docker-compose.yml
services:
# Node.js API
api:
build: ./api
environment:
- DATABASE_URL=postgres://app:secret@db:5432/myapp
# React frontend
frontend:
build: ./frontend
# PostgreSQL
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=myapp
volumes:
- pgdata:/var/lib/postgresql/data
# pgAdmin for DB management
pgadmin:
image: dpage/pgadmin4
environment:
- [email protected]
- PGADMIN_DEFAULT_PASSWORD=admin
# NullBore — expose everything
tunnel:
image: ghcr.io/nullbore/tunnel:latest
environment:
- NULLBORE_API_KEY=nbk_your_project_key
- NULLBORE_TUNNELS=api:8080:api,frontend:3000:app,pgadmin:80:db-admin
depends_on:
- api
- frontend
- pgadmin
restart: unless-stopped
volumes:
pgdata:
docker compose up -d
# ✓ https://api.yourname.nullbore.com → api:8080
# ✓ https://app.yourname.nullbore.com → frontend:3000
# ✓ https://db-admin.yourname.nullbore.com → pgadmin:80
Tips
- One key per project — create a dedicated API key in the dashboard for each Docker project. This keeps tunnels isolated and revocable.
- Use
depends_on— ensure your services are running before NullBore tries to connect. restart: unless-stopped— NullBore reconnects automatically, but a container restart policy handles edge cases.- Protect admin tools — use
--auth(orauth = "user:pass"in config) for services like pgAdmin or database consoles. - TTL for dev sessions — set short TTLs in the config file to avoid leaving tunnels open overnight.
- ARM support — the image runs natively on Raspberry Pi and Apple Silicon (linux/arm64).
Networking notes
NullBore connects outbound to tunnel.nullbore.com:443 over WebSocket. No inbound ports need to be opened on your firewall or router. The tunnel container resolves other container names via Docker's built-in DNS — this works automatically within a Compose project (shared default network).
If your services are on a custom Docker network, make sure the NullBore container is on the same network:
services:
tunnel:
image: ghcr.io/nullbore/tunnel:latest
networks:
- backend
- frontend
environment:
- NULLBORE_API_KEY=nbk_your_key_here
- NULLBORE_TUNNELS=api:8080:api,web:3000:app
networks:
backend:
frontend: