diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index c61ae6f..ec755df 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -30,6 +30,7 @@ export default defineConfig({ text: 'Guide', items: [ { text: 'Getting started', link: '/guide/getting-started' }, + { text: 'Server setup from zero', link: '/guide/server-setup' }, { text: 'Google Chat setup', link: '/guide/google-chat-setup' }, { text: 'Commands', link: '/guide/commands' }, { text: 'Configuration', link: '/guide/configuration' }, diff --git a/docs/guide/deployment.md b/docs/guide/deployment.md index 0335194..cde7e8b 100644 --- a/docs/guide/deployment.md +++ b/docs/guide/deployment.md @@ -4,6 +4,10 @@ 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. +> Starting from zero (no server, no domain, no TLS setup)? Follow +> **[Server setup from zero](./server-setup)** — a complete VPS + Caddy +> walkthrough. This page covers the building blocks and alternatives. + ## Database: embedded or bring your own | Mode | Configuration | When to choose it | diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 1a920ab..09877d0 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -15,8 +15,10 @@ the names of mandatory participants who didn't fill it in. - A Google Workspace domain where you can configure a Chat app (one-time, ~15 minutes — see [Google Chat setup](./google-chat-setup)) -- Somewhere to run one small container with an HTTPS URL - (see [Deployment](./deployment)) +- Somewhere to run one small container with an HTTPS URL — starting from + nothing? **[Server setup from zero](./server-setup)** walks you through a + $5 VPS, a domain, and automatic HTTPS in ~30 minutes + (other options in [Deployment](./deployment)) ## Quickstart diff --git a/docs/guide/server-setup.md b/docs/guide/server-setup.md new file mode 100644 index 0000000..2269c0f --- /dev/null +++ b/docs/guide/server-setup.md @@ -0,0 +1,173 @@ +# Server setup from zero + +Everything from "I have nothing" to AsyncUp running on your own domain with +automatic HTTPS. No prior server experience assumed — about 30 minutes. + +What you'll end up with: + +``` +your team's Google Chat ⇄ https://standup.example.com + │ Caddy (automatic HTTPS) + ▼ + AsyncUp container (SQLite inside) +``` + +## 1. Rent a small server + +Any provider works — Hetzner, DigitalOcean, Vultr, Lightsail, OVH, Oracle +Cloud's free tier… AsyncUp needs very little +(see [system requirements](./deployment#system-requirements)): + +- **1 vCPU, 512 MB+ RAM**, 10 GB disk — usually the cheapest tier (~$4–6/mo) +- **Ubuntu 24.04 LTS** (commands below assume it; Debian works identically) +- amd64 or arm64 — both are published images + +You'll get an **IP address** and SSH access (`ssh root@`). + +## 2. Point a domain at it + +In your DNS provider, create an **A record** for a subdomain pointing at the +server's IP: + +| Type | Name | Value | +| --- | --- | --- | +| A | `standup` | `` | + +So `standup.example.com → `. DNS usually propagates in minutes — +`ping standup.example.com` should answer from your server's IP before you +continue (HTTPS certificates won't issue until it does). + +## 3. Basic hardening + Docker + +SSH in and run: + +```bash +# stay patched automatically +apt-get update && apt-get -y upgrade && apt-get -y install unattended-upgrades ufw + +# firewall: SSH + web only +ufw allow OpenSSH +ufw allow 80/tcp +ufw allow 443/tcp +ufw --force enable + +# Docker (official install script) +curl -fsSL https://get.docker.com | sh +``` + +## 4. Deploy AsyncUp + Caddy + +Caddy is a tiny web server that gets and renews **Let's Encrypt certificates +automatically** — you never touch TLS again. Create a directory with three +files: + +```bash +mkdir -p /opt/asyncup && cd /opt/asyncup +``` + +**`compose.yml`** + +```yaml +services: + asyncup: + image: ghcr.io/asyncup-dev/asyncup:latest + restart: unless-stopped + env_file: .env + environment: + DB_PATH: /data/standup.db + volumes: + - standup-data:/data + + caddy: + image: caddy:2-alpine + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + +volumes: + standup-data: + caddy-data: + caddy-config: +``` + +Note that AsyncUp itself exposes no ports to the internet — only Caddy does. + +**`Caddyfile`** (swap in your domain) + +``` +standup.example.com { + reverse_proxy asyncup:8080 +} +``` + +**`.env`** + +```bash +DASHBOARD_TOKEN=$(openssl rand -hex 24) # run these two commands and paste +SECRET_KEY=$(openssl rand -hex 32) # the values, or use any generator +``` + +i.e. the file should contain two lines like: + +``` +DASHBOARD_TOKEN=2f8c1d… +SECRET_KEY=9b04ee… +``` + +Start it: + +```bash +docker compose up -d +``` + +## 5. Verify + +```bash +curl https://standup.example.com/healthz # → {"ok":true} +``` + +First HTTPS request can take ~30 seconds while Caddy obtains the certificate. +If it doesn't come up: `docker compose logs caddy` — the usual culprits are +DNS not pointing at this server yet, or ports 80/443 blocked by a provider +firewall (some clouds have one *in addition to* ufw). + +Now open **`https://standup.example.com/dashboard?token=`** — +you'll see the first-run checklist. From here, follow +**[Google Chat setup](./google-chat-setup)** (paste your GCP project number +and service-account key into Settings, point the Chat app at +`https://standup.example.com/chat/events`). + +## 6. Updates & backups + +**Update AsyncUp** (new image, schema migrates automatically): + +```bash +cd /opt/asyncup && docker compose pull && docker compose up -d +``` + +**Back up the database** (everything lives in one SQLite file): + +```bash +docker compose cp asyncup:/data/standup.db ./standup-backup-$(date +%F).db +``` + +Drop that line in a cron job and ship the file wherever you keep backups. +Stored secrets in it are encrypted — just keep `SECRET_KEY` (your `.env`) +backed up separately. Restoring = putting the file back and +`docker compose up -d`. + +**Logs:** `docker compose logs -f asyncup` + +## Variations + +- **Your own Postgres** instead of SQLite: add `DATABASE_URL=postgres://…` to + `.env` — see [Deployment](./deployment#database-embedded-or-bring-your-own). +- **Existing reverse proxy** (nginx, Traefik, …): skip Caddy, expose + `asyncup` on a local port and proxy `https://your-domain → localhost:8080`. +- **No server at all**: Cloud Run scale-to-zero — + see [Deployment](./deployment#scale-to-zero-cloud-run).