The production path is one Dockerized Go server behind Caddy on a small VPS. Keep one backend replica until room and session state move out of process memory.
Install:
- Docker Engine with the Compose plugin
- A firewall allowing inbound
80/tcp,443/tcp, and SSH - A DNS
AorAAAArecord pointing the backend hostname to the VPS
Create a deployment directory:
mkdir -p ~/videotogetherCopy deploy/docker-compose.yml, deploy/Caddyfile, and a filled .env file into that directory.
Start from deploy/.env.example.
Required values:
SERVER_IMAGE: immutable GHCR tag, such asghcr.io/yicheny0580/videotogetherlite-server:sha-abcdef0.BACKEND_DOMAIN: hostname only, withouthttps://.CADDY_EMAIL: email used by Caddy for ACME account notices.
Operational values:
ROOM_TTL: room inactivity timeout, default3m.ALLOWED_ORIGINS: CORS and WebSocket origin policy, default*.
The broad origin default is intentional for the current architecture. The page app runs inside arbitrary video pages, so browser requests originate from those page origins. The backend does not use cookies; invite codes and session tokens are bearer values in JSON messages.
cd ~/videotogether
docker compose pull
docker compose up -dCheck health:
curl --fail https://BACKEND_DOMAIN/healthzRun the room and WebSocket smoke test:
BACKEND_PUBLIC_URL=https://BACKEND_DOMAIN just smoke-backendRun the local Docker and Caddy smoke test before relying on the compose assets:
just smoke-dockerThis builds apps/server/Dockerfile, starts the server behind a temporary Caddy container on http://127.0.0.1:18080, runs the backend smoke flow through Caddy, and tears the compose stack down.
The same verifier is available as the manual Deployment Smoke GitHub Actions workflow.
Test local rollback mechanics:
just smoke-rollbackThis starts the Docker stack on a temporary local port, verifies a current image tag through Caddy, rewrites the stack to a previous image tag, verifies the rollback through Caddy, and creates validated current/previous extension ZIP artifacts.
Check logs:
docker compose logs -f server
docker compose logs -f caddyRestart:
docker compose restart serverRollback to a previous image tag:
sed -i.bak 's#SERVER_IMAGE=.*#SERVER_IMAGE=ghcr.io/yicheny0580/videotogetherlite-server:sha-previous#' .env
docker compose pull
docker compose up -d
curl --fail https://BACKEND_DOMAIN/healthzUse the Backend Image workflow to publish an immutable image tag, then run Deploy Backend.
After the target branch or tag is pushed to GitHub, the release workflow runner
can dispatch and watch the expected manual workflows:
RELEASE_CHANNEL=beta IMAGE_TAG=sha-abcdef0 just run-release-workflow ci deployment-smoke backend-image deploy-backend extension-packageFor production tag releases:
RELEASE_CHANNEL=production RELEASE_REF=v3.0.23 just run-release-workflow backend-image deploy-backend extension-packageUse RELEASE_DRY_RUN=1 to print the gh workflow run commands without
dispatching. Chrome Web Store upload is not part of the default sequence; run it
as an explicit step after listing, privacy, and tester settings are ready:
RELEASE_CHANNEL=beta CWS_PUBLISH_TYPE=upload_only just run-release-workflow chrome-web-storeConfigure GitHub Actions environments:
betaproduction
Audit the repository workflow and environment state:
just audit-releaseThis command is read-only. It checks that release workflows exist, the deploy workflow uses environment-scoped concurrency, the beta and production environments exist in GitHub, and the production environment has required reviewers.
Create or repair the expected environments:
PRODUCTION_REVIEWER=yicheny0580 just configure-github-envsThe command is idempotent. It creates beta and production; production gets a required user reviewer. Add secrets and environment variables in GitHub after the environments exist.
Environment secrets:
VPS_HOSTVPS_USERVPS_SSH_KEYBACKEND_PUBLIC_URLif not stored as an environment variableCADDY_EMAILif not stored as an environment variableCWS_SERVICE_ACCOUNT_JSON
Environment variables:
BACKEND_PUBLIC_URLCADDY_EMAILROOM_TTLALLOWED_ORIGINSCWS_PUBLISHER_IDCWS_EXTENSION_ID
Use environment variables for non-secret release inputs and environment secrets for credentials:
gh variable set BACKEND_PUBLIC_URL --env beta --body https://beta.example.com
gh variable set CADDY_EMAIL --env beta --body admin@example.com
gh variable set CWS_PUBLISHER_ID --env beta --body publisher-id
gh variable set CWS_EXTENSION_ID --env beta --body extension-id
gh secret set VPS_HOST --env beta --body vps.example.com
gh secret set VPS_USER --env beta --body deploy
gh secret set VPS_SSH_KEY --env beta < ~/.ssh/videotogether_deploy
gh secret set CWS_SERVICE_ACCOUNT_JSON --env beta --body "$(jq -c . service-account.json)"Repeat for production with its production backend URL, VPS target, and Chrome Web Store item ID.
The Chrome Web Store service account must also be granted access in the Chrome Web Store Developer Dashboard under the publisher account settings.
You can also set the same inputs from local environment variables or a dotenv-style file:
cp .release-inputs.example.env .release-inputs.env
RELEASE_INPUTS_FILE=.release-inputs.env just configure-github-env-inputsEach key can be global, such as BACKEND_PUBLIC_URL, or channel-specific, such as BETA_BACKEND_PUBLIC_URL or PRODUCTION_BACKEND_PUBLIC_URL. Channel-specific keys take precedence. The filled .release-inputs.env file contains secrets and is ignored by git.
Use required reviewers on the production environment. The deploy workflow has per-environment concurrency so two production deploys cannot race.
Caddy terminates TLS and proxies plain HTTP to server:8080. The beta
deployment expects the backend hostname to be proxied through Cloudflare, so
Caddy uses an internal origin certificate and Cloudflare presents the public
edge certificate to browsers. WebSocket upgrades are handled by reverse_proxy.
The Caddyfile uses /healthz as the active upstream health check.