withvibe
Self-hosting

Routing — ports vs subdomains

Every env exposes one or more services. withvibe supports two ways to reach them: host port mapping (good for solo / local) and per-env subdomains via Traefik (good for shared / cloud deployments).

Quick comparison

 Port modeSubdomain mode (Traefik)
URL shapehttp://localhost:51234https://web.env-a3f1c2.example.dev
DNS requiredNoWildcard *.<baseDomain> → host
TLSPlain HTTPTraefik + ACME wildcard cert
Concurrent envsBounded by free ports / collisionsEffectively unlimited
Best forLocal dev box, single user, exporting envsCloud, shared team, customer demos
Port mode

Direct host port → container

The orchestrator allocates a free port per env. The browser connects directly to the host on that port; Docker does the rest.

Browser
http://host:51234
Host network
:51234
web container
:3000
  • · Each env gets a unique host port from the allocator.
  • · No DNS, no certificates. Plain HTTP on a port.
  • · Two envs publishing the same internal port = different host ports — no collision.
Subdomain mode

Traefik routes by hostname

DNS resolves *.example.dev to your host. Traefik terminates TLS, reads the Host header, and forwards to the right container over a Docker network — never via published host ports.

Browser
https://web.env-a3f1c2.example.dev
Wildcard DNS
*.example.dev
Traefik
TLS termination + routing
web container
:3000 (internal)
  • · No host ports exposed — services live on the proxy Docker network.
  • · Every env gets its own subdomain prefix derived from its id.
  • · TLS via ACME (Let's Encrypt) with a wildcard cert.

Port mode

The default. The orchestrator's port allocator picks free host ports for every service marked with ports: or a system-port variable. It records the allocation on the env so the same service stays on the same port across restarts.

Set PUBLIC_HOST in the API process env (defaults to localhost). It can be a hostname or an IP — anything teammates on the same network can reach.

# api.env
PUBLIC_HOST=localhost          # or 192.168.1.50, dev-box.lan, etc.

Service URLs in templates get rendered as http://$${PUBLIC_HOST}:<port>. Local exports always force PUBLIC_HOST=localhost regardless of the server setting.

Subdomain mode

Each env gets a short identifier (last 6 characters of its CUID, lowercase). Services are published as <service>.env-<short>.<baseDomain> via Traefik running in front of the Docker daemon.

Configure these on the workspace:

  • routingMode: "subdomain"
  • routingBaseDomain: the base domain you control DNS for, e.g. example.dev
DNS prerequisite
Subdomain mode needs a wildcard DNS record (*.<baseDomain> → <your host>) and, if you want HTTPS, a wildcard certificate. Without them, browsers can't resolve env URLs.

How the rewrite works

When subdomain mode is active, the compose rewriter does the following per service:

  1. Detects whether the service is “exposed”: it has ports:, x-expose: true, or pre-existing traefik.enable=true labels. Explicit x-expose: false always wins.
  2. Strips the ports: stanza — Traefik talks to the service over the internal Docker network, not via published ports.
  3. Attaches the service to two networks: the external proxy network (where Traefik lives) and a per-env internal network.
  4. Adds Traefik labels: traefik.enable=true, traefik.http.routers.<id>.rule=Host(`...`), and traefik.http.services.<id>.loadbalancer.server.port=<internal port>.
  5. Records the resulting service → URL map so the UI can link to it.

The internal port is chosen in this order: an existing traefik...server.port label (template author's intent) → the first ports: entry → an expose: entry → an x-port hint → 80 as last resort.

What withvibe rewrites

From your compose file to a Traefik-aware one

# your template
services:
  web:
    image: my/web:latest
    ports:
      - "${WEB_PORT}:3000"
# materialized for subdomain mode
services:
  web:
    image: my/web:latest
    networks: [proxy, internal]
    labels:
      traefik.enable: "true"
      traefik.http.routers.env-a3f1c2-web.rule: "Host(`web.env-a3f1c2.example.dev`)"
      traefik.http.services.env-a3f1c2-web.loadbalancer.server.port: "3000"

The ports: stanza is dropped, two networks are attached, and Traefik labels are injected. Internal port resolution priority: existing label → portsexpose x-port80.

Reference Traefik service

Run a single Traefik instance per host that joins the external proxy network. Minimal config:

# traefik.yml
services:
  traefik:
    image: traefik:v3
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --providers.docker.network=proxy
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.le.acme.dnschallenge=true
      - --certificatesresolvers.le.acme.dnschallenge.provider=cloudflare
      - [email protected]
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - proxy

networks:
  proxy:
    external: true

Create the external network once, before bringing up either Traefik or any env:

docker network create proxy

Choosing a mode

  • Run port mode ifyou're evaluating withvibe, running it solo on a laptop, or your team uses local exports as the primary workflow.
  • Run subdomain mode if envs are shared with non-technical teammates, customers, or sales prospects. URLs that look like https://demo.env-a3f1c2.acme.dev are dramatically friendlier than http://10.0.4.12:51234, and you avoid port collisions altogether.

Switching modes

Routing mode is a workspace-level setting. Existing envs keep their current routing on restart — they don't silently re-materialize. Recreate the env (or run Recompose) to pick up the new mode.