Deployment reference
AsyncUp is one small container — by default with SQLite inside, so there are no external moving parts at all. Google Chat needs to reach it on a public HTTPS URL.
Looking for a walkthrough? Step 1: Host covers every scenario — VPS, Postgres, Cloud Run, homelab — with a decision matrix and production checklist, and Operations covers upgrades and backups. This page is the building-blocks reference.
Database: embedded or bring your own
| Mode | Configuration | When to choose it |
|---|---|---|
| Embedded SQLite (default) | DB_PATH on a persistent volume | Simplest possible ops: one container, one file to back up. Plenty for any team size AsyncUp serves |
| Bring your own PostgreSQL | DATABASE_URL=postgres://… | You already run managed Postgres (RDS, Cloud SQL, Neon, Supabase, …) and want its backups/HA, or your platform has no persistent volumes (e.g. some scale-to-zero setups) |
| Postgres on the same machine | docker compose --profile postgres up -d + DATABASE_URL=postgres://asyncup:…@postgres:5432/asyncup | Postgres semantics without leaving the box |
Setting DATABASE_URL skips SQLite entirely; the schema is created and migrated automatically on startup in both modes. The full test suite runs against both engines in CI on every change.
System requirements
AsyncUp idles at a once-a-minute scheduler tick; load is a few webhook calls per person per day. CPU architecture: amd64 or arm64.
| Setup | Minimum | Recommended |
|---|---|---|
| App + SQLite | 1 vCPU (shared is fine), 256 MB RAM, 1 GB disk | 1 vCPU, 512 MB RAM, 5 GB disk |
| App + bundled Postgres | 1 vCPU, 768 MB RAM, 3 GB disk | 2 vCPU, 2 GB RAM, 10 GB disk |
| App with external/managed DB | 1 vCPU, 256 MB RAM | 1 vCPU, 512 MB RAM |
Realistic sizing: the Bun process uses ~100–150 MB RSS; the image is ~300 MB; a year of standups for a 50-person team is well under 100 MB of data. The smallest VPS tier (or Cloud Run's 512 MB default) is comfortably enough — teams into the hundreds of users don't change this picture.
Prebuilt image
Multi-arch images (linux/amd64, linux/arm64) are published to GHCR on every merge to main:
docker pull ghcr.io/asyncup-dev/asyncup:latestTags: latest (main), vX.Y.Z + X.Y (releases), sha-… (every build) — pin a digest for reproducible deploys. Every image carries the commit it was built from in the org.opencontainers.image.revision label. Building from source stays a one-liner (docker build -t asyncup .) if you prefer auditing what you run.
Docker Compose (simplest)
cp .env.example .env # set OPERATOR_TOKEN + SECRET_KEY
docker compose up -d # pulls the GHCR image by default
# then finish setup in https://HOST/app (sign in with the operator token)The repo's compose file publishes port 8080 on 127.0.0.1 only, so a reverse proxy on the same machine reaches it and the internet doesn't. Don't widen it to every interface: Docker's port publishing bypasses host firewalls such as ufw. Path A publishes nothing but Caddy. Put it behind any HTTPS reverse proxy (Caddy, nginx, Traefik) and point the Chat app at https://HOST/chat/events. The SQLite data lives in the standup-data volume.
A 1-shared-CPU VPS or a free-tier VM is plenty — the bot is idle except for a once-a-minute scheduler tick and your team's submissions.
Scale-to-zero (Cloud Run)
Closest thing to "no servers at all", and realistically $0/month at team scale on the free tier:
- Deploy the container to Cloud Run (min instances = 0, max instances = 1 — the in-process scheduler must not run twice). Use a managed Postgres via
DATABASE_URL(Cloud SQL, or Neon's free tier) — Cloud Run's filesystem is ephemeral and network volumes don't support SQLite locking reliably. - The in-process scheduler only runs while an instance is alive, so drive it externally: create a Cloud Scheduler job (free tier covers it) that hits
POST /tickevery minute with headerAuthorization: Bearer <tick token>. - Generate the tick token under Settings › API & tokens.
Webhook events (dialog opens, submissions, commands) spin the instance up on demand; /tick wakes it for prompts, reminders, and deadlines. Ticks are idempotent — overlapping or missed ticks are safe and caught up on the next one.
Bare Bun
bun install --frozen-lockfile --production
PORT=8080 DB_PATH=/var/lib/asyncup/standup.db bun src/index.tsRun it under systemd or any process manager.
Production checklist
- [ ] Guided setup finished at
/app/setup: project number and service-account key verified, the events URL saved as an audience, first signed Chat event received - [ ] HTTPS in front of
/chat/events(Google only delivers to https) - [ ] Google or SAML sign-in configured, then the setup token switched off under Settings › Sign-in & SSO
- [ ] Tick token generated if
/tickis internet-reachable;SECRET_KEYbacked up - [ ]
DB_PATHon persistent storage, backed up (orDATABASE_URLpointing at managed Postgres) — Operations