Deployment
Telescreen is one container with one volume. Nothing else is required: no database server, no queue, no sidecar. This page describes what the shipped Compose file does and how to put HTTPS in front of it.
The Compose service
services:
telescreen:
build: .
image: telescreen:latest
container_name: telescreen
restart: unless-stopped
ports:
- "127.0.0.1:8788:8788"
env_file:
- path: .env
required: false
volumes:
- ./data:/data| Piece | Effect |
|---|---|
build: . + image: | Builds the local Dockerfile and tags the result telescreen:latest |
container_name | Fixed name, so docker compose logs telescreen and docker exec are predictable |
restart: unless-stopped | Comes back after a reboot or crash, stays down when you stop it deliberately |
ports: 127.0.0.1:8788:8788 | Published on loopback only — not reachable from other machines until a proxy exposes it |
env_file (optional) | Extra variables from .env; a missing file is not an error |
volumes: ./data:/data | The only persistent state, owned by uid 1000 inside the container |
The image is a two-stage build on node:22-bookworm-slim. The build stage compiles the server with tsc and the web app with Vite, then prunes dev dependencies; the runtime stage carries only dist/, web/dist/, drizzle/, and production node_modules, and runs as the unprivileged node user. It declares VOLUME /data and EXPOSE 8788.
Reaching PantryPal
The compose file uses only its own default network, so a fresh clone starts without any network that must already exist. Meals still need a route from the container to PantryPal. The simplest option is PantryPal's tailnet or LAN URL:
PANTRYPAL_URL=https://pantrypal.your-tailnet.ts.netIf both stacks run on the same host and you prefer Docker's internal DNS, join PantryPal's network with a docker-compose.override.yml next to the compose file. Compose merges it automatically:
services:
telescreen:
networks: [default, shared]
networks:
shared:
name: pantrypal_default # check with: docker network ls
external: trueThen set PANTRYPAL_URL=http://pantrypal:8787 (use the container name and the port PantryPal listens on). PantryPal must publish no port for this to work, but the network and container must already exist when Telescreen starts.
HTTPS
The container speaks plain HTTP. Terminate TLS in front of it.
Tailscale (documented path)
tailscale serve --bg 8788tailscale serve binds to the tailnet interface, obtains a certificate for https://<machine>.<tailnet>.ts.net, and proxies to http://127.0.0.1:8788. Because it is bound to the tailnet, the app is reachable only by devices in your tailnet.
On a port other than 443:
tailscale serve --bg --https=8444 http://127.0.0.1:8788
# then in .env:
TELESCREEN_PUBLIC_URL=https://machine.your-tailnet.ts.net:8444Include the port in TELESCREEN_PUBLIC_URL, otherwise provider redirect URIs are built for 443 and OAuth fails with a redirect mismatch.
Your own reverse proxy
Any proxy that can reach 127.0.0.1:8788 works — Caddy, nginx, Traefik. Keep the instance on a private network anyway: Telescreen assumes the network is the access boundary, and has no login screen. Set TELESCREEN_PUBLIC_URL to the external HTTPS URL so OAuth callbacks land on the right host, and forward Host and X-Forwarded-* unchanged.
If the proxy is not on the tailnet, use every other control you have (VPN, firewall rules, tokens with TELESCREEN_TRUST_NETWORK off). See Access and security.
Checking a deployment
curl -s http://127.0.0.1:8788/api/v1/health
# {"ok":true,"version":"0.1.0"}
docker compose ps # STATUS should report (healthy)
docker inspect --format '{{json .State.Health}}' telescreen | jq/api/v1/health is public and answers with { ok, version }. The image's HEALTHCHECK polls it every 30 seconds with a 5-second timeout, a 10-second start period, and 3 retries, so a fresh container reports starting briefly before turning healthy.
docker compose ps showing Up (healthy) together with an unreachable page almost always means the proxy, not the app — see Troubleshooting.
Changing ports
Two ports matter and they are independent:
- Inside the container
PORT=8788is set by the Composeenvironmentblock and by the image. Change it only if you also change the mapping. - On the host the mapping
127.0.0.1:8788:8788decides the local port. To publish9000instead, write127.0.0.1:9000:8788and pointtailscale serveat 9000.
Binding to 0.0.0.0 on the host ("8788:8788") removes the loopback protection. Do that only when a firewall restricts who can reach the port.
Moving to another machine
- Stop the container.
- Copy the whole data directory, including
telescreen.db-walandtelescreen.db-shm. - Move
TELESCREEN_SECRET_KEYwith it. - Start the same image on the new host, with the same
TELESCREEN_PUBLIC_URLor a new one whose OAuth redirect URIs you have updated.
Procedure and caveats: Backups and restore. Updating an existing deployment: Updates.