Private HTTPS with Tailscale Serve
The recommended HTTPS deployment uses the official Tailscale image as a separate
container alongside Open WebIDE. Serve acts as a reverse proxy: it terminates
HTTPS, manages certificates, and forwards HTTP and WebSocket traffic to the app
and bridge. Phones and other clients connect through Tailscale using a trusted
https://<device>.<tailnet>.ts.net address. No custom domain, DNS records or
router port forwarding are needed.
Tailscale remains independently installed and updated. The Open WebIDE image contains the app and execution bridge; it does not contain Tailscale.
Prerequisites
- Install Tailscale on each client device and join the same tailnet.
- Enable MagicDNS and HTTPS Certificates in the tailnet's DNS settings.
- Create a non-ephemeral auth key for the server container. Supply it through
the
TS_AUTHKEYenvironment variable at startup; keep it out of committed files. - Choose the node name
open-webideand find your tailnet DNS suffix in the Tailscale admin console. Substitute the full hostname below. If that name is already in use, choose a unique name and updateTS_HOSTNAMEtoo.
See Tailscale Serve and container configuration for authentication, certificate and networking options.
Docker deployment
Use docker-compose.https.yml on its own. It publishes no host ports: the app and
bridge bind to loopback inside Tailscale's network namespace. The app database,
workspace and Tailscale identity remain in separate persistent volumes/mounts.
export OPENWEBIDE_TAILSCALE_HOST=open-webide.tailNNNN.ts.net
export OPENWEBIDE_SERVE_CONFIG_DIR="$HOME/.config/openwebide/tailscale"
mkdir -p "$OPENWEBIDE_SERVE_CONFIG_DIR"
sh docker/tailscale/serve-config.sh "$OPENWEBIDE_TAILSCALE_HOST" \
> "$OPENWEBIDE_SERVE_CONFIG_DIR/serve.json"
# Set TS_AUTHKEY in your shell; do not put it in a committed file.
docker compose -f docker-compose.https.yml up -d --build
docker compose -f docker-compose.https.yml exec tailscale tailscale status
docker compose -f docker-compose.https.yml exec tailscale tailscale serve status
Set OPENWEBIDE_WORKSPACE if your projects are outside $HOME/source. Set
OPENWEBIDE_TAILSCALE_NAME if using a node name other than open-webide. The full
hostname in OPENWEBIDE_TAILSCALE_HOST must match the enrolled node; a conflicting
node name may acquire a suffix. After enrollment, you can remove TS_AUTHKEY and
recreate the container with the same state volume. Recreate the app too whenever
recreating Tailscale, so they share the current namespace.
The mounted Serve JSON is declarative and survives container recreation. Serve
strips /bridge before forwarding to port 3001; it preserves the browser Origin
and the public Host for the bridge's existing allowlist checks. / forwards to
Spin, including REST and streaming responses. It supports WebSocket upgrades at
/bridge as well as bridge HTTP requests such as /bridge/health. The
backend-only secret bootstrap route is not exposed through the proxy.
Open https://$OPENWEBIDE_TAILSCALE_HOST. New browser configurations automatically
use wss://<same-host>/bridge. An existing explicit Settings → Bridge URL is
retained; change it to this route if migrating from the old port 8443 setup.
Settings → Install app offers the browser install prompt when available, or
browser-specific instructions. On iPhone/iPad, use Safari's Share → Add to Home
Screen. HTTP LAN origins show the HTTPS setup guidance; HTTP localhost qualifies
as a secure context.
Serve is private tailnet access; this configuration explicitly disables Funnel.
Rootless Podman
The same topology works without TUN devices or additional capabilities: run the official userspace Tailscale container first, then join its network namespace. Use your existing database/workspace mounts when migrating; never run two app instances against the same database.
podman build -t open-webide:local .
podman run -d --name open-webide-tailscale \
-v openwebide-tailscale:/var/lib/tailscale \
-v "$OPENWEBIDE_SERVE_CONFIG_DIR:/config:ro,Z" \
-e TS_AUTHKEY -e TS_AUTH_ONCE=true -e TS_USERSPACE=true \
-e TS_HOSTNAME=open-webide -e TS_STATE_DIR=/var/lib/tailscale \
-e TS_SERVE_CONFIG=/config/serve.json tailscale/tailscale:stable
podman run -d --name open-webide \
--network container:open-webide-tailscale \
-v openwebide-data:/app/.spin -v "$HOME/source:/workspace:Z" \
-e OPENWEBIDE_APP_HOST=127.0.0.1 -e OPENWEBIDE_BRIDGE_HOST=127.0.0.1 \
-e OPENWEBIDE_BRIDGE_ALLOWED_HOSTS="$OPENWEBIDE_TAILSCALE_HOST" \
-e OPENWEBIDE_BRIDGE_ALLOWED_ORIGINS="https://$OPENWEBIDE_TAILSCALE_HOST" \
open-webide:local
podman exec open-webide-tailscale tailscale serve status
Integrate the containers into systemd/Quadlet for restart management; the app guide covers data persistence. Keep Tailscale outside the app image and lifecycle. On SELinux hosts use the bind mount labels shown above.
Native deployment
Run the native app on port 3000 and bridge on 127.0.0.1:3001. Before starting the bridge, set its allowed public hostname and browser origin:
export OPENWEBIDE_BRIDGE_ALLOWED_HOSTS="$OPENWEBIDE_TAILSCALE_HOST"
export OPENWEBIDE_BRIDGE_ALLOWED_ORIGINS="https://$OPENWEBIDE_TAILSCALE_HOST"
tailscale serve --bg --https=443 http://127.0.0.1:3000
tailscale serve --bg --https=443 --set-path=/bridge http://127.0.0.1:3001
tailscale serve status
These background Serve settings persist in Tailscale's state. Both commands use port 443, yielding the same-origin app and bridge routes. Explicit old Bridge URL settings still need to be updated as described above.
Alternative: Caddy with an internal CA
Clients must trust Caddy's internal CA. Use your chosen hostname in the bridge
Host/Origin allowlists and replace webide.home here:
webide.home {
tls internal
handle /bridge/secret {
respond "Not available through the proxy" 403
}
@bridge path /bridge /bridge/*
handle @bridge {
uri strip_prefix /bridge
reverse_proxy 127.0.0.1:3001
}
handle {
reverse_proxy 127.0.0.1:3000
}
}
Keep app/bridge listeners on loopback. The proxy handles TLS, SSE and WebSockets; no TLS certificates need to be installed in the bridge. PWA installation depends on the browser trusting the certificate, not merely the presence of HTTPS.
Verification
The repeatable transport smoke test uses Caddy's internal CA and the same shared network-namespace topology. It verifies real TLS, secure session cookies, REST, SSE and authenticated WebSockets in both workspace modes, checks proxy request guards, and restarts the app to verify persistence. Run it after building:
python3 docker/https-test/check.py docker --image open-webide:local
# Use the same image in Podman, then run the rootless contract:
docker save open-webide:local | podman load
python3 docker/https-test/check.py podman --image open-webide:local --port 8447
Both engine contracts have passed. Live verification of the official Tailscale container deployment also passed in Docker, including trusted certificates, secure cookies, REST file write/read/delete, incremental SSE and authenticated WSS in both workspace modes. Native Tailscale Serve passed the same transport checks. Chrome received an install prompt over the trusted tailnet HTTPS origin, with an active service worker that excludes API and bridge response caching.
Recreating the Tailscale and app containers with their existing volumes preserved the node ID, hostname, IP addresses, Serve JSON and account database without re-enrollment. Both-mode transport checks passed again after recreation. The checks used an isolated node, account, workspace and deterministic model provider.
After deployment, check /bridge/health, sign in, and confirm the bridge connects.
Open a remote project, write/read/delete a temporary file, then repeat with a local
folder. Check terminals, approval prompts and a streaming chat response in each
mode. These exercise REST, SSE and WebSocket forwarding and browser file access.
Disable the server/network and reload: the app should show Can't reach the
server, without showing cached project or conversation data. Restore the
connection and use Retry.
The service worker caches only versioned shell assets and its offline screen. API responses and bridge traffic are never cached. Existing open windows retain their worker until closed; reopen the app after an upgrade to activate the new build.