The live instance
The instance maintained by the author runs the same image as any self-hosted instance—the
repository’s docker-compose.yml, on a VPS, behind Caddy with TLS. This document covers what
it adds to INSTALACAO.md, the baseline procedure that applies here in full.
What already works
Section titled “What already works”-
Continuous integration in
.github/workflows/ci.yml: every push tomainand every pull request runspnpm lint,pnpm typecheck,pnpm test, action tests against a real Postgres, andpnpm test:e2e(Chromium, desktop and mobile), and builds and starts the Docker image against a fresh database—migration, public page with server-side RDKit, engine withimmutable, promotion script. The Playwright report is uploaded as an artifact. -
Published image at
ghcr.io/gabrielmello1407/rotameron everyv*tag (.github/workflows/image.yml):git tag v0.3.0git push --tagsIt receives the tags
0.3.0,0.3, andlatest. GHCR packages start private: after the first publication, make it public in the package settings on GitHub—otherwise,docker compose pullreturnsdeniedfor people without an account.
What the live instance adds
Section titled “What the live instance adds”
1. Domain
Section titled “1. Domain”Still not confirmed with the registrar. See the caveat in DECISOES.md D-07: no DNS record
does not mean availability at a registrar, and availability at a registrar does not mean
availability at INPI. Until there is an address, README.md does not promise a link.
2. Caddy in front, with the engine precompressed
Section titled “2. Caddy in front, with the engine precompressed”prebuild writes .br and .gz files beside every file in public/chem/, and this determines
how an entire class’s first lesson goes—measured, not estimated:
| File | Original | gzip | brotli |
|---|---|---|---|
RDKit_minimal.wasm |
6753 KB | 1998 KB (−70%) | 1437 KB (−79%) |
ocl-resources.json |
1320 KB | 457 KB | 375 KB |
RDKit_minimal.js |
125 KB | 29 KB | 26 KB |
The Next server compresses with gzip on the fly. To serve the ready-made brotli files, Caddy needs them on the host’s disk—they come from the image itself:
docker compose cp app:/app/apps/web/public/chem ./chemAnd the Caddyfile:
rotamer.exemplo.br { encode zstd gzip
handle_path /chem/* { root * /srv/rotamer/chem file_server { precompressed br gzip } header Cache-Control "public, max-age=31536000, immutable" }
reverse_proxy 127.0.0.1:3000}Repeat cp after every update that changes the RDKit or OpenChemLib version—prebuild prints
both versions during the build.
immutable handles return visits even without a proxy: 605 ms with the engine cached,
versus 6,9 s on the first visit. For thirty students in the same classroom, that is the
difference between starting the lesson and waiting to start.
3. Backup in cron, with a scheduled rehearsal
Section titled “3. Backup in cron, with a scheduled rehearsal”The VPS has the Postgres client installed, and the repository’s scripts run from the host
against 127.0.0.1:5432, where Compose exposes the database:
# once a day, at 3 a.m., in the crontab of the user running Compose0 3 * * * DATABASE_URL='postgresql://rotamer:<senha>@127.0.0.1:5432/rotamer' BACKUP_REMOTE='usuario@outra-maquina:/backups/rotamer/' /srv/rotamer/scripts/backup-db.sh >> /var/log/rotamer-backup.log 2>&1The script dumps in Postgres’s custom format, checks that the file opens, sends a copy off the machine, and deletes backups older than 30 days. Any failed step aborts with an error—cron sends the email, and silence is no longer a sign of success.
The rehearsal, once a month and whenever the schema changes:
createdb -h 127.0.0.1 -U rotamer rotamer_ensaio # once; pg_restore does not create a databaseDATABASE_URL='postgresql://rotamer:<senha>@127.0.0.1:5432/rotamer' ./scripts/restore-db.sh /var/backups/rotamer/rotamer-<carimbo>.dumpThis restores into rotamer_ensaio, never over production—that requires explicitly setting
RESTORE_CONFIRMO=sim. At the end, the script prints the three counts that show whether the
backup is worth anything: profiles, attempts, and nicknames. A backup that has never been
restored is a file, not a guarantee (D-11).
4. Updating
Section titled “4. Updating”cd /srv/rotamerDATABASE_URL='postgresql://rotamer:<senha>@127.0.0.1:5432/rotamer' ./scripts/backup-db.shgit pulldocker compose pulldocker compose up -dMigrations run when the container starts, before it opens its port. The order is always:
backup, new code, startup. If migration fails, the app does not start and the log explains why
(docker compose logs app); Postgres keeps running with the data intact.
5. Telemetry
Section titled “5. Telemetry”Self-hosted Umami, in another Compose deployment on the same VPS, behind the same Caddy. It
exists because an observation session without measurement becomes an anecdote: you can see
one person get stuck, but cannot tell whether they are typical. Enable it with the two .env
variables; what is measured, and what never leaves, is described in INSTALACAO.md.
6. Teacher and administrator accounts
Section titled “6. Teacher and administrator accounts”As on any instance: through the script, inside the container.
docker compose exec app node scripts/promote-teacher.mjs ana@escola.br --escola "EE Dom Pedro II"docker compose exec app node scripts/promote-teacher.mjs ana@escola.br --escola "EE Dom Pedro II" --administradorA teacher without a school filled in cannot issue anything—the code is only valid for someone at the same school, and the script refuses promotion when the field is empty (D-19).
On the live instance, run the second command: one administrator per school. From then on, the coordinator promotes their own teachers through the interface, without coming here—and still cannot create another administrator, which keeps control of the instance with the person who has the machine (D-29).
7. What to check when something goes down
Section titled “7. What to check when something goes down”docker compose ps—the app showshealthywhen/marcaresponds.docker compose logs --tail 200 app—migration, Next startup, route errors.- If the screen stays on
carregando o motor(loading the engine), the first suspect is/chem/*: the image was built withoutprebuild, or Caddy’shandle_pathpoints at an empty directory. - WASM loads after the first paint, inside the worker. The 3 s target for the first drawing on a low-end phone over 3G depends on this remaining true.
Costs, and what happens if they become unaffordable
Section titled “Costs, and what happens if they become unaffordable”A small VPS, a domain, and the Gemini key for the tutor. If they someday become unaffordable,
the instance scales down—the tutor turns off first, the instance goes away last—and the code
does not become closed (D-28). Anyone needing guaranteed availability should run their own
instance: that is why INSTALACAO.md exists.