Skip to content

Deployment Runbook

Production runs on a Sakura VPS (8 GB) at saikokustudio.com with a self-hosted MongoDB (see project notes). This page lists everything you must do after deploying a code release so that the server does not crash and existing data stays consistent.

TL;DR

  1. Pull the new code and restart the service.
  2. From the matching repo root, run the required scripts for any PR whose number you have not run yet.
  3. Verify the health endpoints respond 200.

If a Python ValidationError or KeyError shows up at runtime right after deploy, the most likely cause is that a required one-shot script was skipped. Check the table below.


Required one-shot scripts

All scripts are idempotent. Re-running them is safe. Every script supports --dry-run, which prints what would happen without modifying the DB.

PRRepoCommand (from repo root)Required?What it does
#85 PR1butler-platformcd auth/backend && uv run python -m scripts.seed_tax_rate_masterYesSeeds 3 default rates (10% main, 8% reduced, 0% exempt) into tax_rate_master. Without this the Studio Admin dropdown is empty and no corporate can pick a main rate.
#85 PR3butler-taxcd backend && uv run python -m scripts.cleanup_system_settings_tax_ratesOptionalRemoves the orphaned system_settings["tax_rates"] MongoDB document (no longer read). Skipping it leaves stale data but does not break anything.
#85 PR4butler-taxcd backend && uv run python -m scripts.migrate_tax_category_to_structuredYesConverts the legacy "課税仕入 10%" strings in journal_rules / tax_firm_journal_rules / receipts / invoices / system_settings.journal_map into the structured {type, rate} form expected by the new code. Without this, existing rules will fail Pydantic validation and the API will return 500. The script makes a <collection>__backup__<ts> copy before modifying anything.
#88butler-taxcd backend && PYTHONPATH=. uv run python scripts/migrate_email_cc_split.pyOptionalSplits the legacy corporate_email_settings.cc_mode + always_cc_emails into independent always_cc_emails / always_bcc_emails lists. The resolver has a legacy fallback so this is not required for correctness, but running it normalises the stored data to the new schema.
#175 (S1)butler-platformcd auth/backend && uv run python -m scripts.seed_account_masterYesSeeds the standard chart of accounts (109 accounts covering the 4 TestData companies) into account_master. Idempotent — existing codes are never overwritten, so operator edits survive re-runs. Required before the journal/reporting engine (#175 S2+) can classify entries.
減価償却 D1butler-platformcd auth/backend && uv run python -m scripts.seed_asset_useful_livesYes (for the fixed-asset register, D2+)Seeds the statutory useful-life categories and the NTA official depreciation-rate tables (別表第八 straight-line / 別表第十 200% DB / 別表第九 250% DB, lives 2–50; the 200%/250% switch is by acquisition date) plus the small-asset / SME special-measure amount thresholds into asset_useful_lives / depreciation_rates / depreciation_thresholds. Idempotent and non-destructive — Studio Admin corrections survive re-runs; *_250 fields are backfilled only when physically absent. Rate values are flagged for tax-accountant confirmation.
#175 (S5)butler-taxcd backend && uv run python -m scripts.backfill_auto_journalOptionalGenerates draft journal entries from already-approved receipts/invoices and confirmed matches (--corporate-id <id> to limit to one corporate). Idempotent — sources with an existing entry (source_type+source_id unique index) are skipped. New activity is journalized automatically by hooks, so this is only needed to backfill history; drafts do not affect reports until confirmed in 仕訳帳.
#90 (Slice B) + system emailsbutler-platformcd auth/backend && uv run python -m scripts.seed_email_template_masterOptional (recommended)Seeds platform-default email templates into email_templates: document emails (invoice/estimate/receipt × ja/en) and system/invitation emails (tax-firm & sales-agent invitation, sales-agent login, email-change confirm/notice, law-watch — ja & en). Row-level, safe to re-run: inserts missing (type, locale) rows, refreshes rows still authored by seed_script to the latest coded default, and never overwrites Studio-Admin edits (rows whose updated_by is a real uid). Re-run after deploy to pick up new/updated default rows. email_service falls back to coded defaults (email_default_templates.py) when a row is absent, so seeding is not required for correctness, but it lets Studio Admin edit the shared defaults. Requires INTERNAL_SERVICE_TOKEN for the /internal/email-templates read path used by butler-tax.
Permission matrixbutler-taxcd backend && uv run python -m scripts.backfill_employee_permissionsYesBackfills new-shape employees.permissions (sections/capabilities/features) by snapshotting each employee's role-group default from the Platform Tier0 template (GET /internal/permission-defaults). Preserves any existing taxAdvisorChat into features.tax_advisor_chat; drops legacy dataProcessing/reportExtraction. Idempotent (skips employees already in the new shape). Supports --dry-run. Requires INTERNAL_SERVICE_TOKEN shared with butler-platform. Without it, existing users have empty permissions once the consumption side gates pages.

Running order on a fresh production server

bash
# 1. butler-platform: seed the new tax-rate master
cd butler-platform/auth/backend
uv run python -m scripts.seed_tax_rate_master --dry-run   # preview
uv run python -m scripts.seed_tax_rate_master              # apply

# 2. butler-tax: migrate the tax_category strings to structured form
cd butler-tax/backend
uv run python -m scripts.migrate_tax_category_to_structured --dry-run
uv run python -m scripts.migrate_tax_category_to_structured

# 3. butler-tax: drop the orphaned tax_rates document
cd butler-tax/backend
uv run python -m scripts.cleanup_system_settings_tax_rates

Environment variables checklist

Verify these before bringing up the new release. Missing values cause the server to fail at startup with an EnvironmentError or to crash on first request with a KeyError.

butler-platform/auth/backend (port 8003)

VariableRequiredNotes
MONGODB_URIyesmongodb://...
MONGODB_DB_NAMEyesbutler_platform in production
INTERNAL_SERVICE_TOKENyesShared secret with butler-tax / butler-law. Must match the value those services use.
FIREBASE_CREDENTIALS_PATHyesPath to the service-account JSON file. The file must exist on disk and be readable by the service user.
STRIPE_SECRET_KEYyesLive key in production
STRIPE_WEBHOOK_SECRETyesFrom the production Webhook endpoint (see #57)
CORS_ORIGINSyesComma-separated list including the Studio Admin / auth-frontend / butler-tax origins
LAW_API_URLyes (#161)butler-law backend URL (http://localhost:8002 if co-located). The daily revision-watch job polls GET /api/internal/revisions with INTERNAL_SERVICE_TOKEN.
STUDIO_ADMIN_URLyes (#161)https://admin.saikoku-studio.com in production — link target in revision-watch notification emails.
SENDGRID_API_KEYyesFor invitation / notification emails. Without it sends are skipped with a warning (dev default).
SENDGRID_FROM_EMAILyesFallback FROM only — the operational FROM lives in the email_settings master, editable on the Studio Admin メールテンプレ page, which validates against SendGrid verified senders on save (unverified addresses fail every send with HTTP 403; only billing@saikokustudio.com is verified today). Complete SendGrid domain authentication (DNS) before using a noreply@ address.

butler-tax/backend (port 8001)

VariableRequiredNotes
MONGODB_URIyesself-hosted MongoDB URI
MONGODB_DB_NAMEyestax_agent in production (test uses tax_agent_test)
PAYROLL_MONGODB_URInoPayroll datastore (#178). Empty → uses MONGODB_URI. Set to isolate payroll on a separate cluster (Atlas / Butler Labor)
PAYROLL_DB_NAMEnoEmpty → <MONGODB_DB_NAME>_payroll. Payroll is physically separated from business data; classification etc. live here, only the journal crosses into the main DB
PLATFORM_API_URLyeshttp://localhost:8003 if co-located, otherwise the platform URL
INTERNAL_SERVICE_TOKENyesMust equal the platform's value
FIREBASE_CREDENTIALS_PATHyesSame service-account JSON; needed for Firestore PII collections (company_profiles, bank_accounts, clients)
ANTHROPIC_API_KEYyesFor Butler Chat
GOOGLE_API_KEYyesFor Gemini OCR
ENVIRONMENTyesproduction enables stricter startup checks (e.g. INTERNAL_SERVICE_TOKEN required)

butler-law/backend (port 8002)

VariableRequiredNotes
MONGODB_URIyes
MONGODB_DB_NAMEyesbutler_law in production
PLATFORM_API_URLyes
INTERNAL_SERVICE_TOKENyesSame shared secret. Guards GET /api/internal/revisions (revision feed polled by the platform's law-watch, #161).
ANTHROPIC_API_KEYyes

butler-platform/studio_admin/frontend (port 3004)

VariableRequiredNotes
NEXT_PUBLIC_PLATFORM_API_URLyesPlatform API origin; rewrites proxy /api/* to it
LAW_API_URLyesbutler-law backend origin; rewrites proxy /api/law/* to it (#156: Law admin UI lives here)

If ENVIRONMENT=production and any required variable is missing for butler-tax, startup raises:

RuntimeError: INTERNAL_SERVICE_TOKEN is required in production

That is intentional. Fix the env var rather than removing the check.


Native dependencies

WeasyPrint (PDF rendering, butler-tax)

The PDF generation path (invoice_pdf_service) links against libpango, libcairo, and libgdk-pixbuf at runtime. On Debian/Ubuntu install before starting the service:

bash
apt-get update
apt-get install -y \
  libpango-1.0-0 \
  libpangoft2-1.0-0 \
  libharfbuzz0b \
  libfontconfig1 \
  libcairo2 \
  libgdk-pixbuf-2.0-0 \
  libffi8 \
  shared-mime-info

Missing any of these surfaces as a Python OSError: cannot load library at the first PDF request, not at startup. So a server can look healthy until someone clicks "PDF を生成".

When containerising, mirror this list into the Dockerfile.


Health checks after deploy

ServiceURLExpected
butler-platformhttps://saikokustudio.com/health200 {"status":"ok"}
butler-taxhttps://saikokustudio.com/api/v1/health200
butler-lawhttps://saikokustudio.com/law/health200
Studio Admin (frontend)https://admin.saikoku-studio.com200 (or 307 to login)

If any of these returns 502 / 500 and you have not run the scripts above for a freshly deployed PR, run them first before debugging further.


When this page should be updated

Every PR that ships:

  • a new one-shot script (under scripts/ in either repo), or
  • a new required environment variable, or
  • a new system-level dependency (apt package, native library, container layer),

must add a row to the appropriate table above in the same PR. If you skip this step, the next person who deploys will hit the exact issue you would have wanted documented.

Treat this page as the single source of truth for what "deploy a release" means in this project.

Butler Series — Saikoku Studio