biga

Networking

Reach your biga over IPv4 and IPv6, on an intranet, over a VPN, or behind a proxy you already run.

This page assumes the Docker setup. The same guide ships with it as docs/networking.md.

How traffic flows

Browsers and the biga CLI connect to Caddy on TCP 80 and 443 and UDP 443. Caddy sends /api/sessions requests to the API server and everything else to the website. The server talks to Postgres on an internal network that has no route out and no published port.

Caddy is the only container with published ports. Postgres is unreachable from the host and the network.
  • Only Caddy is exposed. It terminates TLS and routes requests.
  • Postgres is on an internal network, with no route out and no published port.
  • The server trusts only Caddy for a client's address, and Caddy sends exactly one: the real client, after its own BIGA_UPSTREAM_PROXIES. That address drives rate limits, and IPv6 clients are counted per /64.

The CLI and the viewer share one URL, BIGA_PUBLIC_URL: the CLI uploads to /api/sessions/... on it and prints BIGA_PUBLIC_URL/session/<id>.

Pick a mode

ModeUse it forCertificatesClients need
publicA domain on the internetLet's Encrypt, automaticNothing
internalOffice LAN, homelab, VPN, a bare IPCaddy's local CATo trust the CA once
httpA network you fully trust, or TLS terminated in frontNoneNothing

./scripts/setup.sh asks for the mode and the name or address and writes the rest. What follows is what to set up around it.

IPv4 on the internet

Point DNS at the host

Create an A record for your domain with the host's public IPv4 address.

Open the ports

Allow inbound TCP 80 and 443, and UDP 443 for HTTP/3, in your cloud provider's firewall and on the host. Port 80 is required: Let's Encrypt validates through it, and Caddy redirects it to HTTPS. Behind a home router, forward the same ports to the host's LAN address and give the host a fixed LAN address.

Check from outside

curl -4 -sI https://biga.example.com/ | head -1

Carrier-grade NAT

If your router's WAN address is in 100.64.0.0/10, or differs from what curl -4 ifconfig.co reports, your ISP shares one IPv4 address between customers and inbound IPv4 cannot reach you. Use IPv6, a VPN, a tunnel such as Cloudflare Tunnel, or a small VPS.

Docker bypasses ufw

Docker publishes ports with its own iptables rules, which run before ufw's, so ufw deny 443 does not close a port Docker published. Bind Caddy to one address with BIGA_BIND_ADDRESS, or filter in the DOCKER-USER chain, which Docker evaluates first:

sudo iptables -I DOCKER-USER -i eth0 ! -s 203.0.113.0/24 -j DROP

IPv6

biga works over IPv6 end to end: Caddy, the server and the CLI all handle it, and Let's Encrypt validates over IPv6.

Add an AAAA record

Use the host's stable global address. On Linux, ip -6 addr show scope global lists them; skip any marked temporary, which rotate.

Open the firewall

IPv6 has no NAT, so there is nothing to forward: allow inbound TCP 80 and 443 and UDP 443 to the host, on the host firewall and on the router. Many routers drop unsolicited inbound IPv6 by default.

Turn on IPv6 inside Docker

docker compose down
docker compose -f compose.yaml -f compose.ipv6.yaml up -d

Docker publishes ports on [::] either way, but on an IPv4-only network it relays IPv6 connections through a userland proxy, and Caddy sees every IPv6 visitor as the Docker gateway: one shared rate-limit budget, and logs that say nothing. With IPv6 on the edge network, connections are routed natively and keep their addresses.

Docker Engine 27 or newer picks a unique local /64 by itself. On older engines set "ipv6": true and "ip6tables": true in /etc/docker/daemon.json and choose a subnet in compose.ipv6.yaml. Add COMPOSE_FILE=compose.yaml:compose.ipv6.yaml to .env so every later command includes the file.

Check both families

curl -6 -sI https://biga.example.com/ | head -1
curl -4 -sI https://biga.example.com/ | head -1

Brackets around IPv6 addresses

Bare IPv6 addresses go in brackets wherever a URL or port is involved: BIGA_PUBLIC_URL=https://[2001:db8::10], BIGA_BIND_ADDRESS=[2001:db8::10], BIGA_API_URL=https://[2001:db8::10]. setup.sh adds them for you.

An IPv6-only host serves only IPv6 clients. Offer both families if viewers might be on IPv4-only networks, which is still common on mobile and in offices.

An office or home network

Choose a name

  • A record on your local DNS server, router, Pi-hole or dnsmasq: biga.internal → 192.168.1.20. Prefer .internal, which is reserved for private networks, or home.arpa. Avoid .local, which belongs to mDNS.
  • The host's mDNS name, such as studio.local. macOS, most Linux desktops and Windows 10+ resolve it without a DNS server.
  • A bare address: 192.168.1.20, or [fd00::20] for IPv6.

Set up in internal mode

./scripts/setup.sh --mode internal --domain biga.internal
docker compose up -d --build
./scripts/trust-ca.sh

trust-ca.sh exports the root certificate of Caddy's local certificate authority. Install it once on every device that opens the site or runs the CLI; the script prints the command for each OS. After that HTTPS works with no warnings, and the CLI connects as it would to a public server.

Listen only on the LAN

If the host has other interfaces, such as a public NIC or a VPN, bind to the LAN address in .env:

BIGA_BIND_ADDRESS=192.168.1.20

Keep access keys on: anyone on the network can reach the server, and a key decides who may broadcast through it. If many people reach it through one NAT address, raise BIGA_READ_RATE_LIMIT or set it to 0.

HTTP mode on a LAN

http mode skips the CA but sends owner tokens and access keys in clear text. Use it only on a network you would trust with them.

No internet at all

Build the images on a connected machine and carry them over:

docker compose build
docker save biga/server:local biga/web:local postgres:17-alpine caddy:2-alpine \
  | gzip > biga-images.tar.gz
# on the air-gapped host
gunzip -c biga-images.tar.gz | docker load
docker compose up -d

Nothing is pulled (BIGA_PULL_POLICY defaults to never). Use internal or http mode, since Let's Encrypt needs the internet.

A VPN

Treat a Tailscale, WireGuard or ZeroTier network like an intranet. Bind to the host's VPN address so the server answers only over the VPN, and use internal mode with that address or the VPN's DNS name:

./scripts/setup.sh --mode internal --domain 100.101.102.103 --bind 100.101.102.103

With Tailscale, the MagicDNS name (studio.tail1234.ts.net) works as the domain too.

Behind a proxy you already run

When nginx, Traefik or a load balancer already owns ports 80 and 443, run the stack in http mode on a loopback port and proxy to it:

./scripts/setup.sh --mode http --domain biga.example.com --bind 127.0.0.1 --http-port 8088

Set BIGA_PUBLIC_URL to the public https:// address, and set BIGA_UPSTREAM_PROXIES to the address Caddy sees your proxy from (through a loopback port that is the Docker bridge gateway; docker network inspect biga_edge shows it). The proxy must stream server-sent events and accept large uploads:

nginx
location / {
    proxy_pass http://127.0.0.1:8088;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;          # live updates
    proxy_read_timeout 1h;        # long-lived streams
    client_max_body_size 64m;     # snapshot uploads
}

Behind Cloudflare's proxy, keep public mode and set BIGA_UPSTREAM_PROXIES to Cloudflare's published IP ranges, or every visitor looks like Cloudflare.

Ports

PortProtocolPurpose
80TCPRedirect to HTTPS, Let's Encrypt validation; everything in http mode
443TCPHTTPS
443UDPHTTP/3 (optional; clients fall back to TCP)

BIGA_HTTP_PORT and BIGA_HTTPS_PORT change the host side. Public mode needs the defaults, because Let's Encrypt validates on port 80.

Troubleshooting

SymptomLikely cause
Caddy logs certificate errorsDNS does not point here yet, or port 80 is blocked or forwarded elsewhere. See docker compose logs caddy.
certificate signed by unknown authorityInternal mode without the CA on that device. Run ./scripts/trust-ca.sh.
The CLI says cannot reach …Firewall, a missing forward, or the wrong BIGA_API_URL. Try curl -v with the same URL on the same machine.
401 access_key_requiredSet BIGA_ACCESS_KEY to one of the server's keys.
Viewers stuck on "Reconnecting"A proxy in front is buffering the event stream. Turn buffering off.
Every client has the same address in the logsIPv6 without compose.ipv6.yaml, Docker Desktop, or a proxy missing from BIGA_UPSTREAM_PROXIES.
Caddy answers 502 after adding an override fileThe stack was not recreated. Run docker compose down, then up again.
Works on IPv4 but not IPv6No AAAA record, or a router or host firewall dropping inbound IPv6.

Docker Desktop

Fine for trying biga out, but it runs containers in a VM: Caddy sees every client as the VM's gateway, and inbound IPv6 is limited. Deploy on Linux.