Deploy tide
tide is a simple, lightweight video conference service that you host yourself. It is one program. The web app and the media server run inside it.
This guide puts tide on one Linux server. For Kubernetes, see Kubernetes.
You need
- A Linux server with Docker and a public IPv4 address.
- A DNS name for the server. This guide uses
meet.example.com.
1. Open the ports
| Port | For |
|---|---|
80/tcp, 443/tcp |
the web app |
7882/udp |
audio and video |
7881/tcp |
audio and video when UDP is blocked |
2. Get tide
mkdir /srv/tide && cd /srv/tide
git clone https://github.com/Convex-Works/tide.git src3. Write the configuration
/srv/tide/.env:
TIDE_BASE_URL=https://meet.example.com
TIDE_ADDR=127.0.0.1:8080
TIDE_TRUSTED_PROXIES=127.0.0.1/srv/tide/compose.yaml:
services:
caddy:
image: caddy:2
command: caddy reverse-proxy --from ${TIDE_BASE_URL} --to 127.0.0.1:8080
network_mode: host
volumes:
- caddy-data:/data
restart: unless-stopped
tide:
build: src
env_file: .env
network_mode: host
volumes:
- tide-data:/data
restart: unless-stopped
stop_grace_period: 20s
volumes:
caddy-data:
tide-data:Caddy gets the HTTPS certificate. Browsers need HTTPS to use the camera.
4. Start tide
docker compose up -d --build
docker compose logs tide | grep 'tide: 'The log shows tide: anonymous, …. Anyone who opens https://meet.example.com can make a room. tide deletes a room 24 hours after its last use.
5. Check tide
curl -fsS https://meet.example.com/healthz # ok
curl -s -o /dev/null -w '%{http_code}\n' https://meet.example.com/rtc/validate # 401Make a room. Join it from a phone on mobile data. Make sure that you see and hear each other.
Add sign-in
With sign-in, only your users can make rooms, and rooms stay. Guests join with the room link.
In your OpenID Connect provider, make a client with a secret. Set its redirect URI to
https://meet.example.com/api/auth/callback. Allow the scopesopenid profile email groups.Add these lines to
.env. Use the issuer URL exactly as the provider shows it.TIDE_OIDC_ISSUER=https://id.example.com TIDE_OIDC_CLIENT_ID=tide TIDE_OIDC_CLIENT_SECRET=<client secret> # openssl rand -base64 48 TIDE_SESSION_SECRET=<random>Run
chmod 600 .envanddocker compose up -d.
To let only some groups sign in, set TIDE_USER_GROUPS. See Configuration.
Add recording
Recording needs sign-in.
Make an S3 bucket. Make an access key that can get, put and delete objects in
recordings/.Add these lines to
.env:TIDE_S3_ENDPOINT=https://s3.eu-central-1.amazonaws.com TIDE_S3_REGION=eu-central-1 TIDE_S3_BUCKET=<bucket> TIDE_S3_ACCESS_KEY=<access key> TIDE_S3_SECRET_KEY=<secret key> # openssl rand -hex 8 TIDE_MEDIA_API_KEY=<random> # openssl rand -hex 32 TIDE_MEDIA_API_SECRET=<random> # openssl rand -hex 32 TIDE_RECORDER_REDIS_PASSWORD=<random> TIDE_RECORDER_TEMPLATE_URL=http://127.0.0.1:8080/egress-templateAdd Redis and the recorder to
compose.yaml, underservices:redis: image: redis:7.4.8-alpine3.21 network_mode: host user: '999:1000' environment: REDISCLI_AUTH: ${TIDE_RECORDER_REDIS_PASSWORD} command: - sh - -ec - | umask 077 printf 'requirepass %s\n' "$$REDISCLI_AUTH" > /tmp/redis.conf exec redis-server /tmp/redis.conf --bind 127.0.0.1 --port 6379 --save '' --appendonly no healthcheck: test: ['CMD-SHELL', 'redis-cli -h 127.0.0.1 ping | grep -q PONG'] interval: 2s retries: 30 restart: unless-stopped recorder: image: livekit/egress:v1.13.0 network_mode: service:tide depends_on: redis: condition: service_healthy environment: LIVEKIT_API_KEY: ${TIDE_MEDIA_API_KEY} LIVEKIT_API_SECRET: ${TIDE_MEDIA_API_SECRET} EGRESS_CONFIG_BODY: | ws_url: ws://127.0.0.1:7880 redis: address: 127.0.0.1:6379 password: "${TIDE_RECORDER_REDIS_PASSWORD}" health_port: 8081 cap_add: [SYS_ADMIN] security_opt: [seccomp:unconfined] shm_size: 1gb tmpfs: - /recordings:mode=1777 restart: unless-stoppedRun
docker compose up -d.Record 30 seconds of a call. Download the recording from the dashboard.
Kubernetes
You need Kubernetes 1.29 or later, a node with a public IPv4 address, an ingress, and a registry.
Build and push the image from the repository:
docker buildx build --platform linux/amd64 -t <registry>/tide:<version> --push .Make the namespace and the secrets. Keep nothing else in this namespace: tide uses host ports, and the recorder needs
SYS_ADMIN. Also make a TLS secrettide-tlsfor the host.kubectl create namespace tide kubectl label namespace tide pod-security.kubernetes.io/enforce=privileged kubectl label node <node> tide/media=public kubectl -n tide create secret generic tide-secrets \ --from-literal=session-secret="$(openssl rand -base64 48)" \ --from-literal=oidc-client-secret=<client secret, or unused> # Only for recording: kubectl -n tide create secret generic media-secrets \ --from-literal=api-key="$(openssl rand -hex 8)" \ --from-literal=api-secret="$(openssl rand -hex 32)" \ --from-literal=recorder-redis-password="$(openssl rand -hex 32)" kubectl -n tide create secret generic s3-secrets \ --from-literal=access-key=<access key> \ --from-literal=secret-key=<secret key>Make
deploy/overlays/production/kustomization.yaml:apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: tide resources: - ../../k8s - ingress.yaml components: - ../../k8s/recording # only for recording images: - name: registry.example.com/tide newName: <registry>/tide newTag: <version> patches: - path: tide.yamlMake
tide.yamlin the same folder. Without sign-in, setTIDE_OIDC_ISSUERto"". For recording, also setTIDE_S3_ENDPOINT,TIDE_S3_PUBLIC_ENDPOINT,TIDE_S3_RECORDER_ENDPOINT,TIDE_S3_BUCKETandTIDE_S3_REGION.apiVersion: apps/v1 kind: Deployment metadata: name: tide spec: template: spec: nodeSelector: tide/media: public containers: - name: tide ports: - containerPort: 7881 hostPort: 7881 protocol: TCP - containerPort: 7882 hostPort: 7882 protocol: UDP env: - name: TIDE_BASE_URL value: https://<host> - name: TIDE_OIDC_ISSUER value: <issuer> - name: TIDE_OIDC_CLIENT_ID value: <client id> --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: tide-data spec: storageClassName: <storage class>Make
ingress.yamlin the same folder. This example is for ingress-nginx. Keep the long timeouts.apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: tide annotations: nginx.ingress.kubernetes.io/proxy-read-timeout: '3600' nginx.ingress.kubernetes.io/proxy-send-timeout: '3600' spec: ingressClassName: nginx tls: - hosts: [<host>] secretName: tide-tls rules: - host: <host> http: paths: - path: / pathType: Prefix backend: service: { name: tide, port: { number: 8080 } }Apply, then do the checks in step 5.
kubectl kustomize deploy/overlays/production | grep -nE '<|example\.com' # must print nothing kubectl apply -k deploy/overlays/production kubectl -n tide rollout status deploy/tide
Run one replica only. Set TIDE_TRUSTED_PROXIES to your ingress pods' addresses, or all visitors share one rate limit. Without a public node, use a UDP load balancer: see the end of deploy/k8s/tide.yaml.
Upgrade
A restart ends all meetings. Upgrade when no meeting runs. Back up first: tide cannot go back to an older database.
git -C src pull && docker compose up -d --buildOn Kubernetes, push a new image, set its tag in kustomization.yaml, and apply.
Back up
With sign-in, tide keeps rooms in /data/tide.db. Do not copy the file while tide runs. Use SQLite's backup:
docker run --rm --volumes-from "$(docker compose ps -q tide)" -v "$PWD:/backup" alpine \
sh -c 'apk add -q sqlite && sqlite3 /data/tide.db ".backup /backup/tide.db"'On Kubernetes, run sqlite3 in a pod that mounts tide-data. Back up the bucket with your provider's tools.
Problems
Read the log first: docker compose logs, or kubectl -n tide logs deploy/tide --all-containers. It names a bad or missing variable.
| Problem | Cause and fix |
|---|---|
| Sign-in shows 502 | tide cannot reach the provider, or TIDE_OIDC_ISSUER is not exact. |
| Calls connect, but there is no video | 7882/udp is closed. Behind NAT, set TIDE_MEDIA_NODE_IP to the public IPv4 address. |
| A recording stays at "starting" | The recorder cannot reach tide. Make sure that both use the same key, secret and Redis password. |
| A recording fails after a minute | The recorder cannot reach the media. Set TIDE_MEDIA_NODE_IP only to an address that is local. |
| A download fails | Browsers cannot reach the bucket. Set TIDE_S3_PUBLIC_ENDPOINT. |
| A person cannot see or hear another | Before they reload, they run tideDiagnostics() in the browser console. It saves a file for you. |
Transcripts
Hosts can transcribe recordings on their own Mac with the moil app. Nothing runs on the server. moil is alpha and unsigned. Its jobs run with the host's permissions.
- Let the bucket key also get, put and delete objects in
transcripts-staging/. - Make sure that
TIDE_S3_PUBLIC_ENDPOINT(orTIDE_S3_ENDPOINT) is HTTPS. - Set
TIDE_TRANSCRIPTS=true.
Hosts open /machines and select Add a machine, or use the command line:
moil pair https://meet.example.com/moil
moil review tide
moil approve tide <hash>
moil agentA job needs up to 10 GB of memory. The first job downloads 2.9 GB.