Deploying with TLS
TLS is not decoration here. Browsers expose the camera and microphone only in
a secure context, so calls simply do not work over http:// on a remote
host, and push notifications need one too. Plan for it before you invite
anyone.
With bundled Caddy
Section titled “With bundled Caddy”For a machine with nothing else on ports 80 and 443.
cp docker/env.example .env # set SLICK_DOMAINdocker compose -f docker/compose.yml --env-file .env up -dCaddy obtains and renews a Let’s Encrypt certificate automatically. Before starting, point an A record at the machine and open 80 and 443. Port 80 is how certificates are issued and renewed, not merely a redirect, so closing it breaks renewal sixty days later.
APP_URL derives from SLICK_DOMAIN, so invite links point at the public
address. Only Caddy publishes ports; the app is reachable through it alone.
On a Laravel Forge box
Section titled “On a Laravel Forge box”Forge provisions and manages nginx and the certificate, so Slick runs behind it rather than bringing Caddy along.
Pick Web server rather than App server when creating it: the app-server type also installs MySQL and PHP, and Slick uses neither — its database is SQLite inside the container.
Forge does not install Docker, so that is the one thing to add by hand:
ssh forge@your-server 'curl -fsSL https://get.docker.com | sh && sudo usermod -aG docker forge'Create the site for the domain in Forge and let it issue a certificate, then:
./docker/deploy.sh --behind-proxy forge@your-serverThat runs the app on 127.0.0.1:3000, which is deliberate: it is unreachable
from the internet even with the firewall open, and only nginx can get to it.
Finally, replace the site’s location / block with the contents of
docker/forge-nginx.conf in Forge’s nginx editor. The Upgrade and
Connection headers there are the part that matters — without them nginx
refuses the WebSocket handshake and the app loads but never receives a
message.
For calls, open 3478 and 5349 on both TCP and UDP in Forge’s firewall, and set
TURN_CERT_DIR in .env to where the certificate lives, usually
/etc/nginx/ssl. Without a certificate the relay still runs on 3478, which is
what carries a call through symmetric NAT; only turns:// is lost.
Deploying without building on the server
Section titled “Deploying without building on the server”The image build runs a full install and compile, which wants more memory than a small VPS has. A workstation can build it instead and ship the result, which also solves the other half of the problem: an Apple Silicon Mac produces an arm64 image, and a rented box is almost always amd64.
cp docker/env.example docker/.env # set SLICK_DOMAIN, at least./docker/deploy.sh root@your-serverThat cross-builds for linux/amd64, streams the image over SSH, copies the
compose files and your .env to /opt/slick, and starts the stack. Run it
again to deploy again.
compose.prebuilt.yml is what makes the server use the shipped image rather
than trying to build one, and it stops Compose looking in a registry for a tag
that only exists on your machine.
Before the first run, two things have to be true, and neither is something the script can do for you: the domain’s A record must already point at the server, because Caddy proves control of it to get a certificate, and port 80 must be reachable, because that is how the certificate is issued and renewed rather than merely a redirect to 443.
Behind a proxy you already run
Section titled “Behind a proxy you already run”A host that already terminates TLS, a Laravel Forge box for example, has something on 80 and 443 already. Do not run Caddy alongside it.
docker compose -f docker/compose.behind-proxy.yml --env-file .env up -dThis publishes to 127.0.0.1:3000 only, so the app is unreachable from the
internet even with the firewall open. Traffic has to come through the proxy.
docker/forge-nginx.conf is a working location block. The WebSocket upgrade
headers in it are load bearing: without them the page loads, looks fine, and no
message ever arrives.
Cloudflare
Section titled “Cloudflare”Slick works behind Cloudflare, with two settings that matter:
- WebSockets must be enabled in Network settings. They are on by default on current plans.
- SSL mode should be Full (strict), with a real certificate on the origin. Flexible mode terminates TLS at Cloudflare and speaks plain HTTP to your server, which puts the origin back in the insecure-context trap.
Note that call media does not pass through Cloudflare. It is peer to peer, or it goes through your own relay.
There is a third thing to know, and it is easy to miss. If Web Analytics is on for the zone, Cloudflare rewrites the HTML of every proxied hostname on the way out to add its tracking script. That includes your Slick. Nothing warns you, and the setting is zone-wide, so turning it on for a marketing site turns it on for the workspace too. Slick’s own policy (below) refuses the script, so you get a console error rather than a tracker. If you would rather not see the error, turn it off under Analytics and Logs, Web Analytics.
The Content-Security-Policy
Section titled “The Content-Security-Policy”The server sends one, and you should leave it alone.
Messages are decrypted in your browser, which means the plaintext lives in this origin. Any script that runs in this origin stands beside it. The policy is what says which scripts may run: your own, and nothing else. That is what makes the paragraph above a console error instead of a third party’s code sitting inside an encrypted workspace.
Two consequences for operators:
- Do not strip or replace it at the proxy. If your proxy adds a policy of its own, browsers enforce both, and the result is the stricter of the two. That is safe. Replacing ours is not.
APP_URLshould be set to the address people actually use. The policy names your WebSocket origin explicitly, because Safari does not treat'self'as coveringwss:and the macOS and iPhone apps are Safari underneath. WithAPP_URLunset the server infers it from the request, which works, but the configured value is the one that cannot be spoofed by a forgedHostheader.