Remote Docker deployment
The Bridge service includes a reusable deployment command for a single remote Docker host. The command uploads a clean source bundle, builds and recreates one bridge node, and verifies the origin and public capability endpoints.
Use a gitignored local wrapper for a concrete hostname, SSH identity, public URL, and signing secret. Do not commit deployment credentials.
Required configuration
From the repository root:
BRIDGE_REMOTE=user@example-host \
BRIDGE_PUBLIC_URL=https://bridge.example.com \
BRIDGE_TOKEN_SECRET=replace-with-a-stable-random-secret \
./bridge-elixir/deploy/remote-docker/deploy.sh
BRIDGE_TOKEN_SECRET signs saved pairing credentials. Keep the same value
across deployments. Replacing it requires frontends to pair again.
Common optional settings:
BRIDGE_REMOTE_DIR=bridge-elixir
BRIDGE_COMPOSE_PROJECT=bridgeelixir
BRIDGE_BIND_HOST=127.0.0.1
BRIDGE_PORT=4000
SSH_OPTS="-i /absolute/path/to/key -o BatchMode=yes"
Use an explicit BRIDGE_COMPOSE_PROJECT when adopting a container created by
an older Compose installation. Every later deployment must use the same
project name.
BRIDGE_BIND_HOST=127.0.0.1 is appropriate when Nginx or another edge on the
same host is the only public entry point. Keep 0.0.0.0 only when direct
trusted-network access to the Cowboy port is intentional.
Gitignored wrapper
A machine-specific wrapper can persist concrete values while the reusable script remains committed:
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
secret_file="${repo_root}/.crystal/bridge-token-secret"
if [[ ! -s "$secret_file" ]]; then
umask 077
openssl rand -hex 32 >"$secret_file"
fi
BRIDGE_REMOTE=user@example-host \
BRIDGE_PUBLIC_URL=https://bridge.example.com \
BRIDGE_COMPOSE_PROJECT=bridgeelixir \
BRIDGE_BIND_HOST=127.0.0.1 \
BRIDGE_TOKEN_SECRET="$(<"$secret_file")" \
SSH_OPTS="-i ${HOME}/.ssh/bridge.pem -o BatchMode=yes" \
"${repo_root}/bridge-elixir/deploy/remote-docker/deploy.sh"
Keep the wrapper and secret file outside Git with permissions 0700 and
0600, respectively. Back up the signing secret securely: losing it makes
existing saved pairings unverifiable after the next deployment.
Deployment behavior
The command:
- requires the stable signing secret and the committed Mix lockfile;
- archives the Bridge service without local builds or dependency caches;
- uploads the source and a private runtime environment file;
- cleans the previous remote source tree so removed files cannot linger;
- selects
docker composeordocker-compose; - builds before replacing the running single-node container;
- waits for origin health and HTTP v1 capabilities;
- verifies the public health and capability endpoints.
Set BRIDGE_VERIFY_LARGE_BODY=1 to require a 1.2 MB request to pass the public
edge and reach the bridge. A 413 response indicates that the edge request
limit is too small.
Set BRIDGE_RUN_EXTERNAL_E2E=1 to run the frontend SDK bridge E2E against the
deployed public endpoint after the deployment checks pass.
Edge requirements
An internet-facing bridge requires HTTPS/WSS. The edge must:
- forward WebSocket upgrades, query strings, and all HTTP methods;
- retain
Authorization,X-Crystal-Bridge-Token, andX-Crystal-Bridge-Body-Present; - accept bodies up to the configured bridge limit;
- disable request/response buffering for streamed transfer paths;
- disable caching;
- use timeouts at least as long as the bridge transfer timeout.
See deploy/remote-docker/nginx-site.example for a complete Nginx example.
After changing Nginx:
sudo nginx -t
sudo systemctl reload nginx
Verification
curl -fsS https://bridge.example.com/health
curl -fsS https://bridge.example.com/bridge/v1/capabilities
The capability response advertises HTTP protocol version 1. For full hosted verification:
BRIDGE_E2E_MODE=external \
BRIDGE_E2E_URL=wss://bridge.example.com/ws/websocket \
npm --workspace @crystal-lattice/frontend-sdk run integration:bridge
External mode verifies legacy WebSocket, HTTP-only, and hybrid operation without restarting the independently managed bridge.