Deploy

September 20, 2026 · View on GitHub

MailboxBoy is meant to run as a single Compose service on a machine you control. The image contains only the binary. Taxonomy, OAuth, the TypeSafe key, and SQLite stay on the host.

What to copy

Never bake these into the image or commit them:

Host fileRole
.envTYPESAFE_API_KEY=…
config/credentials.jsonGmail Desktop OAuth client
config/token.jsonUser refresh token (mailboxboy auth on a machine with a browser)
config/taxonomy.yamlCategories
data/mailbox.dbOptional; keeps history cursor and review rows

Authorize on a laptop first, then copy token.json. The daemon cannot complete the localhost OAuth bounce by itself.

Layout on the server

Example used in production: qctech@host:~/App/MailboxBoy.

MailboxBoy/
  Dockerfile
  docker-compose.yml
  .env                 # gitignored
  config/
    credentials.json
    token.json
    taxonomy.yaml
  data/
    mailbox.db         # optional

docker-compose.yml mounts ./config/config and ./data/data, and sets:

  • MAILBOXBOY_LISTEN=:8080
  • MAILBOXBOY_POLL=5s
  • TZ=Asia/Hong_Kong
  • restart: unless-stopped

The container user is uid 1000. Host files should be owned by that uid (typical qctech / first Linux user). If the bind mount is root-owned, set user: "0:0" in Compose.

Bring-up

On the laptop (after auth + labels init):

mkdir -p config data
cp .env.example .env   # then edit
cp ~/.config/mailboxboy/{credentials.json,token.json,taxonomy.yaml} ./config/
cp ~/.local/share/mailboxboy/mailbox.db ./data/   # optional

rsync -az --exclude '.git' --exclude '.env' ./ user@server:~/App/MailboxBoy/
rsync -az .env user@server:~/App/MailboxBoy/.env
rsync -az config/ user@server:~/App/MailboxBoy/config/
rsync -az data/ user@server:~/App/MailboxBoy/data/

On the server:

cd ~/App/MailboxBoy
chmod 700 config data
chmod 600 config/token.json config/credentials.json .env
docker compose up -d --build
docker compose ps
curl -sS http://127.0.0.1:8080/healthz
curl -sS http://127.0.0.1:8080/status
docker compose logs -f

Healthcheck: wget to /healthz every 30s, start period 20s.

Published port: 8080. Restrict it to LAN or localhost if the host is exposed.

Behaviour after start

  1. Ensures Gmail labels from the taxonomy.
  2. If SQLite already has history_id, only new messageAdded events are classified.
  3. If the database is empty, the cursor is set to now (no 500-message backfill).
  4. Each new INBOX message is sent to Jev and labels are applied (serve defaults to --apply).

Kick without waiting for the poll:

curl -X POST http://127.0.0.1:8080/hooks/sync

With MAILBOXBOY_WEBHOOK_TOKEN:

curl -X POST "http://127.0.0.1:8080/hooks/sync?token=YOUR_TOKEN"

Optional Gmail push

Gmail cannot POST to an arbitrary URL. Instant push requires Google Cloud Pub/Sub:

  1. Create topic gmail-mailboxboy.
  2. Grant gmail-api-push@system.gserviceaccount.com publisher on that topic.
  3. Push-subscribe the topic to https://your-host/hooks/gmail?token=YOUR_TOKEN (HTTPS required).
  4. Set on the container:
    • GMAIL_PUBSUB_TOPIC=projects/PROJECT_ID/topics/gmail-mailboxboy
    • MAILBOXBOY_WEBHOOK_TOKEN=YOUR_TOKEN
  5. Restart. Logs should show gmail watch registered; expires … (watch is renewed daily; Gmail watches last about seven days).

Polling remains the fallback. A 5s poll is enough for a personal inbox if you skip Pub/Sub.

Update

rsync -az --exclude '.git' --exclude '.env' --exclude 'config/' --exclude 'data/' \
  ./ user@server:~/App/MailboxBoy/
ssh user@server 'cd ~/App/MailboxBoy && docker compose up -d --build'

Keep config/ and data/ on the server; do not --delete them.

Backup

  • config/token.json — re-auth if lost
  • data/mailbox.db — history cursor and review notes
  • config/taxonomy.yaml — your category text

Logs and failure

docker compose logs --tail 100

Common cases:

SymptomLikely cause
gmail token missingconfig/token.json not mounted or wrong mode
TYPESAFE_API_KEY is not set.env missing next to Compose file
No label or Classification Label updatesEmpty decision; current builds skip empty messages.modify
Watch errors, poll still worksPub/Sub topic/IAM; safe to ignore if poll is enough
Permission denied on token.jsonuid 1000 cannot write refreshed tokens

Local Compose (no remote)

export MAILBOXBOY_CONFIG="$HOME/.config/mailboxboy"
export MAILBOXBOY_DATA="$HOME/.local/share/mailboxboy"
docker compose up -d --build

Requires Docker Engine. The image is mailboxboy:local.