Self-hosting TCKDB
This guide describes how to run a TCKDB instance on a machine you control — a cloud VM, a lab server, or a single-board computer (e.g. a Raspberry Pi). It covers the two supported deployment shapes, when to pick each, and the cross-cutting good practices (exposure, secrets, backups, migrations).
For applying schema migrations to an already-running database, see the
companion backend/docs/deployment/migrations.md
runbook — everything below assumes that flow for schema changes.
The pieces
A TCKDB deployment has four moving parts:
| Piece | What it is | How it's usually run |
|---|---|---|
| PostgreSQL (RDKit) | the database | a container (stateful, pinned image) |
| MinIO / S3 | artifact object store | a container |
| API | the FastAPI app (uvicorn app.api.app:create_app --factory) |
container or native process |
| Ingress | public TLS + hostname | a Cloudflare tunnel or reverse proxy |
The database and object store are almost always containers — that's the easy, uncontroversial part. The real choice is how you run the API.
Choosing a shape
Pattern A — Fully containerized (recommended default)
Everything (DB, MinIO, API, worker) is a container; docker compose up brings
the whole stack up. The API runs from a published image
(ghcr.io/<org>/tckdbv2/tckdb-api, built by
.github/workflows/build-api-image.yml).
- Pros: maximum reproducibility (the image pins Python + every dependency), one mental model, trivial to move to another host.
- Cons: you need to build/pull the API image. On arm64 (a Raspberry Pi), building that image is slow — see the arm64 note below.
- Use when: you're on an amd64 cloud VM or lab server, or you're willing to build/pull an arm64 image on ARM hardware.
Pattern B — Native API + containerized infra
DB and MinIO are containers; the API runs as a native process under systemd, from a conda/micromamba environment. Ingress via a Cloudflare tunnel.
- Pros: no API image to build — the scientific stack (RDKit, cantera) installs directly from conda-forge as prebuilt binaries, which is much smoother on arm64.
- Cons: the environment is a "snowflake" (built per-host), so it's less perfectly reproducible than an image.
- Use when: you're on constrained/ARM hardware (Raspberry Pi) where building a container image for the scientific stack is painful.
The arm64 note (why Pattern B exists)
An arm64 CPU (Raspberry Pi, Apple Silicon, AWS Graviton) cannot run an amd64 image natively — different instruction sets. It only works through an emulation layer (QEMU on Linux, Rosetta on macOS), which is significantly slower and more memory-hungry — fine for a quick test, wrong for a long-running server. So on arm64 you want a native arm64 image, and building one on an amd64 CI runner requires emulating arm64 (slow). Pattern B sidesteps the build entirely by installing conda-forge's prebuilt arm64 binaries directly on the host. If you do want Pattern A on arm64, build the image on a native arm64 runner (self-hosted or GitHub-hosted arm) rather than via QEMU.
Pattern B walkthrough (native API under systemd)
This is the reference setup for a Raspberry Pi.
1. Infrastructure (containers)
cd <repo>
docker compose --env-file .env.pi --env-file .env.pi.db-admin up -d db minio
.env.pi holds only runtime DB credentials, S3 keys, rate-limit config,
cookie/security settings, etc. .env.pi.db-admin holds the database
administrator and migration-owner credentials. Keep both out of git, make
them mode 600, and never load .env.pi.db-admin into the API service. See
database_roles.md.
2. Environment (native)
Create the conda/micromamba env in a normal env location — not inside the repo:
# Good: central env location.
micromamba create -n tckdb_env -f backend/environment.yml
micromamba run -n tckdb_env pip install -e backend
micromamba run -n tckdb_env pip install -e schemas/python/tckdb-schemas
Do not use
micromamba create -p ./backend/y/envs/tckdb_env— the-p(path prefix) flag puts the whole multi-gigabyte environment inside the source tree. Use-n <name>so it lands in~/micromamba/envs/. (A conda env cannot be relocated withmv— its files bake in absolute paths — so fixing a misplaced env means recreating it at the right location and updating the systemdExecStartpath.)
3. Migrations
cd backend
set -a; source ../.env.pi; set +a
set -a; source ../.env.pi.db-admin; set +a
micromamba run -n tckdb_env python scripts/configure_database_roles.py apply
micromamba run -n tckdb_env alembic upgrade head
micromamba run -n tckdb_env python scripts/configure_database_roles.py check
(See the migrations runbook for the deployed-DB flow: back up first, check
alembic current, apply, verify.)
4. Run the API under systemd
/etc/systemd/system/tckdb-api.service:
[Unit]
Description=TCKDB API (uvicorn)
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=simple
User=<you>
WorkingDirectory=<repo>/backend
EnvironmentFile=<repo>/.env.pi
ExecStart=<env-path>/bin/uvicorn app.api.app:create_app --factory --host 127.0.0.1 --port 8010
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now tckdb-api
systemctl status tckdb-api
journalctl -u tckdb-api -f # logs
Bind the API to 127.0.0.1 (loopback), not 0.0.0.0 — the ingress layer
reaches it locally; nothing else should.
Cross-cutting good practices (both patterns)
Exposure — tunnel, never a raw port
Put the public hostname behind a Cloudflare tunnel (or a reverse proxy you
control). Cloudflare's cloudflared connects outbound from your host to
Cloudflare and maps tckdb.example.com → localhost:8010. Benefits: no
inbound ports opened on your router, TLS terminated by Cloudflare, your home
IP hidden, and a DDoS/WAF layer for free. Never port-forward the API port raw
with a self-signed cert.
Tunnel ingress can be configured in the Cloudflare dashboard (remote-managed)
or in ~/.cloudflared/config.yml (local). Point one hostname at
http://localhost:8010.
Secrets
- The
.envfile is the only place credentials live; keep it out of git andchmod 600. - Rotate any secret that has ever appeared in a chat, ticket, or shell history.
Backups (do this)
Schedule a nightly pg_dump. A systemd timer is clean:
/etc/systemd/system/tckdb-backup.service:
[Unit]
Description=TCKDB Postgres backup (pg_dump)
After=docker.service
[Service]
Type=oneshot
User=<you>
ExecStart=<repo-or-home>/tckdb_backup.sh
/etc/systemd/system/tckdb-backup.timer:
[Unit]
Description=Run TCKDB backup daily
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install]
WantedBy=timers.target
tckdb_backup.sh:
#!/usr/bin/env bash
set -euo pipefail
DEST=<backup-dir>; mkdir -p "$DEST"
FILE="$DEST/tckdb_$(date +%Y%m%d_%H%M%S).sql.gz"
docker exec <db-container> pg_dump -U <user> -d <db> | gzip > "$FILE"
find "$DEST" -name 'tckdb_*.sql.gz' -mtime +14 -delete
sudo systemctl enable --now tckdb-backup.timer
systemctl list-timers tckdb-backup.timer
Offsite copy. Backups that live only on the same disk as the database die with it. Add an
rsync/rclonestep to copy each dump to another machine or a cloud bucket.
Artifacts (MinIO) are a separate backup concern; mirror the object store too if you rely on it.
Migrations on a deployed DB
Follow backend/docs/deployment/migrations.md:
back up → alembic current → read revision docstrings → apply step-by-step →
verify. Never drop-and-recreate a deployed DB.
CI/CD (Pattern A)
build-api-image.yml builds and
publishes a multi-arch image to ghcr.io on every push to main that touches
the backend. That is the build half. The deploy half is deliberately
left to the operator, because it involves a choice and a migration step:
- Pull + restart on the host — options: a small webhook that runs
docker compose pull tckdb-api && docker compose up -d tckdb-api; a tool like Watchtower; or a scheduleddocker compose pull. - Run migrations as part of the deploy — e.g. a one-off
docker compose run --rm tckdb-api alembic upgrade headbefore the new API starts serving. Do not let a new image serve against an un-migrated DB.
Pin image tags by digest in production so a deploy is an explicit, reviewable
change rather than a moving :latest.
Quick reference
| Task | Command |
|---|---|
| Bring up infra | docker compose --env-file .env.pi --env-file .env.pi.db-admin up -d db minio |
| API status / logs | systemctl status tckdb-api · journalctl -u tckdb-api -f |
| Restart API | sudo systemctl restart tckdb-api |
| Apply migrations | alembic upgrade head (env sourced; back up first) |
| Manual backup | sudo systemctl start tckdb-backup.service |
| Backup schedule | systemctl list-timers tckdb-backup.timer |