Deployment
This guide is for putting TimeCues on the open internet — a shared URL your
team (or the public) can reach, instead of localhost on your own machine. If
you only want to run it on your own computer, you don’t need any of this; see
How to run instead.
The short version: TimeCues is a standard Docker Compose application, so “deploying” it means running that same stack on a server that has a public address, with two things added on top — HTTPS (so browsers and Google sign-in trust it) and a persistent disk (so your songs and annotations survive a restart). Any host that runs Docker will do: a cloud VM, a rented box, a machine in your office, or a homelab.
What you’ll end up with
- The TimeCues web app reachable at a real
https://…address. - Google sign-in working, so each person gets their own annotator namespace.
- All audio, annotations, and caches stored on a disk that you can back up.
Before you start
You need three things:
- A host that runs Docker with Docker Compose v2. 2 vCPUs and ~4 GB of RAM are enough for the core app; add more RAM if you’ll run the heavy Demucs/All-In-One models (see Optional heavy models below).
- A domain name (or any hostname) that points at the host’s public IP. HTTPS certificates and Google sign-in are tied to a hostname, so a bare IP address won’t do.
- A Google OAuth client ID whose authorized origin matches that
hostname. The app ships with a demo client that only trusts
localhost, so for any other address you register your own — it’s free, and the steps are in the user guide’s sign-in section.
Step by step (any cloud VM)
The walkthrough below uses a single cloud virtual machine, which is the simplest production setup. The commands are the same on Google Cloud, AWS, Azure, DigitalOcean, or a box under your desk — only the “create a VM” step differs per provider.
-
Create the VM and open ports 80 and 443. Pick a Linux image (Debian or Ubuntu is fine). In the firewall, allow inbound HTTP (80) and HTTPS (443) from anywhere; everything else stays closed.
-
Point your domain at the VM. Create a DNS
Arecord for your hostname (e.g.timecues.example.com) pointing at the VM’s public IP address. DNS can take a few minutes to propagate. -
Install Docker on the VM. Follow Docker’s official install guide for your Linux distribution, then confirm
docker compose versionworks. -
Clone the repo and configure it.
Terminal window git clone https://github.com/sapirca/timecues-studio.gitcd timecues-studiocp .env.example .envEdit
.envand set two values (see Configuration below for the full list):DATA_DIR— an absolute path on a persistent disk, e.g./var/lib/timecues/data. This is where every song and annotation lives, so it must not be a throwaway location.VITE_GOOGLE_CLIENT_ID— your own OAuth client ID from the step above.
-
Start the stack.
Terminal window docker compose up -d --buildThis builds the images on first run (a few minutes) and starts the web app plus its analysis services. The web app listens on port 5173 inside the Docker network — you don’t expose that to the world directly; the reverse proxy in the next step does.
-
Put HTTPS in front with a reverse proxy. A reverse proxy terminates HTTPS on ports 80/443 and forwards requests to the app on 5173. The easiest option is Caddy, which obtains and renews a free Let’s Encrypt certificate automatically — a two-line config is all it takes:
timecues.example.com {reverse_proxy localhost:5173}Run Caddy on the host (or as another container), and it will provision the certificate the first time someone visits your hostname over HTTPS. Traefik or nginx + certbot work just as well if you already use them.
-
Open your hostname and claim the workspace. Visit
https://timecues.example.com, click Start a new dataset, and sign in with Google. The first person to sign in becomes the admin and can then invite the rest of the team from the Team page. Your deployment is live.
Configuration
Everything is set in .env (copied from .env.example). The defaults boot a
working install; for a public deployment you normally only touch the first
two:
| Variable | What it’s for |
|---|---|
DATA_DIR | Absolute host path where audio, annotations, and caches persist. Point it at a mounted disk you back up — not the repo folder or the OS root. |
VITE_GOOGLE_CLIENT_ID | Your Google OAuth client ID. Required on any hostname other than localhost; remember to add your hostname to the client’s Authorized JavaScript origins or Google rejects the sign-in popup. |
HOST_UID / HOST_GID | Linux hosts only. Set these to your user’s id -u / id -g so files written into DATA_DIR aren’t owned by root. |
The phone address (optional)
The web app gives phones its own layout (see the user guide’s
On a phone). Phones get it on your main hostname
too, but the app picks the layout from the hostname — anything starting with
m. is the phone one — so an m. hostname makes it a link you can hand out,
and keeps the desktop address free of it. (In development the same rule gives
you http://m.localhost:5173/, with no setup: every *.localhost name
resolves to 127.0.0.1 and Vite serves it.) To serve one:
- DNS: point
m.your-domain.orgat the same IP (a wildcard*record covers it). - Reverse proxy and host check: the hostname needs its own site in the
proxy — in the bundled Caddyfile that is
TIMECUES_MOBILE_DOMAIN, which Caddy serves beside the main hostname and gets a certificate for — and it has to be listed inVITE_ALLOWED_HOSTS(comma-separated) or Vite answers 403. Until the proxy has a site for the name it has nothing to present at the TLS handshake, which a CDN in front reports as a failed handshake (Cloudflare: error 525). A CDN-proxied hostname still gets a normal Let’s Encrypt certificate — Cloudflare, for one, exempts the ACME challenge path from its HTTPS redirect, so the challenge reaches the origin. - Google sign-in: add
https://m.your-domain.orgto the OAuth client’s Authorized JavaScript origins. It is a separate origin from the main hostname, so without it Google rejects the sign-in popup there.
The full per-platform, per-profile reference — including local development without Docker, Apple Silicon notes, and every flag — lives in INSTALL.md.
Optional heavy models
The base stack covers manual/auto-guess annotation, MSAF, ruptures, BPM detection, and custom detectors — enough for most teams. Two heavier capabilities are opt-in, because they need much more disk and RAM:
- Stem separation + All-In-One (Demucs) — add
--profile demucs-cpu(works anywhere) or--profile demucs-gpu(needs an NVIDIA GPU, much faster) to yourdocker compose upline. - Experimental MIR detectors — add
--profile experimental-models.
Profiles are additive, so a deployment matching the public hosted instance is:
docker compose --profile demucs-cpu --profile experimental-models up -d --buildSwitching profiles later is just docker compose down followed by a new up
line — your data under DATA_DIR is untouched. The detailed trade-offs
(image sizes, speeds, host requirements) are in
INSTALL.md.
Updating a running deployment
To pull in new code:
git pulldocker compose up -d --buildThe rebuild reuses cached layers, so updates are usually quick, and your
DATA_DIR is never touched. If you want this to happen automatically on every
push, wire a small CI job (GitHub Actions, Google Cloud Build, or a webhook)
that SSHes to the host and runs those two commands — that’s exactly how the
public hosted instance redeploys itself.
Backing up
Everything that matters lives in one place: the DATA_DIR you configured.
Snapshotting or copying that directory captures the entire corpus — songs,
annotations, annotator profiles, and the corpus config — because the rest of
the app is stateless code you can rebuild from the repo at any time. The
regenerable algorithm caches inside it can be safely dropped to save space;
which files are precious versus rebuildable is spelled out in the
Data model.
Troubleshooting
- Google sign-in popup is rejected / closes immediately. Your hostname
isn’t on the OAuth client’s Authorized JavaScript origins list, or
VITE_GOOGLE_CLIENT_IDstill points at the demo client. Fix both and rebuild. - Certificate won’t provision. The hostname’s DNS
Arecord must resolve to this host, and ports 80 and 443 must be open to the internet — Let’s Encrypt validates over both. - Files in
DATA_DIRare owned by root (Linux). SetHOST_UID/HOST_GIDin.envand recreate the containers.
More fixes — slow first build, ports already in use, containers exiting silently — are in INSTALL.md → Troubleshooting.