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 (-p flags)
  • 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

VariableRequiredDescription
NULLBORE_API_KEYYour API key (from dashboard)
NULLBORE_SERVERNoRelay server URL (default: https://tunnel.nullbore.com)
NULLBORE_TUNNELSNoStatic 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 (or auth = "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: