Skip to content

Deployment

Matt Dula edited this page Apr 18, 2026 · 3 revisions

Deployment

Nakatomi runs anywhere a Dockerfile + Postgres can run. Four supported paths.

Read this first: docs/DEPLOYMENT_LESSONS.md captures the eleven gotchas we hit on our first Railway deploy (shell wrapping, migration ordering, driver mismatch, duplicate Postgres, mcp

  • pydantic version coupling, healthcheck timing, buffered stdout, log CLI quirks). If you're deploying to a new cloud target for the first time, walk the checklist at the top of that doc before anything else.

Railway (recommended)

%%{init: {"look": "handDrawn", "theme": "dark"}}%%
flowchart LR
    GH[GitHub repo] -->|push| Build[Railway build]
    Build --> Img[Docker image]
    Img --> App[Nakatomi service]
    Img -.shares vars.-> Worker[same image<br/>webhook worker]
    App --> PG[(Railway Postgres)]
    App --> Vol[(Railway volume<br/>/app/data)]
    App -.->|optional| S3[(S3 / R2)]
Loading
  1. Fork the repo or connect it in the Railway dashboard.
  2. Add a Postgres plugin — Railway sets DATABASE_URL automatically.
  3. Add a Volume mounted at /app/data (for file storage with STORAGE_BACKEND=local). Skip if using S3.
  4. Set env vars:
    • SECRET_KEYopenssl rand -hex 32
    • STORAGE_BACKENDlocal or s3
    • S3_BUCKET, S3_REGION, S3_ENDPOINT_URL, S3_ACCESS_KEY, S3_SECRET_KEY (if S3)
    • MEMORY_CONNECTORS — empty or a comma list
    • any adapter-specific vars (see Memory-Connectors)
  5. Deploy. The Dockerfile runs alembic upgrade head then uvicorn.

The railway-template.json in the repo pre-declares all of this. Once you publish it at railway.app/new/template, you get a one-click button for the README.

Docker Compose (local / VPS)

git clone https://github.com/mrdulasolutions/NakatomiCRM.git
cd NakatomiCRM
cp .env.example .env
# set SECRET_KEY to openssl rand -hex 32
docker compose up -d --build

The install.sh wrapper adds --seed and --dashboard conveniences:

./install.sh --seed you@example.com --dashboard

The stack is a single docker-compose.yml with postgres:16-alpine and the Nakatomi image. Named volumes persist data:

  • nakatomi_pg — Postgres datadir
  • nakatomi_files — uploaded files (local backend)

Reset:

docker compose down -v   # wipes both volumes

Native Python

For dev, debugger-friendly:

docker run -d --name nk-pg -e POSTGRES_PASSWORD=nakatomi \
  -e POSTGRES_USER=nakatomi -e POSTGRES_DB=nakatomi \
  -p 5432:5432 postgres:16
pip install -r requirements.txt
alembic upgrade head
python -m scripts.seed --email you@example.com --password secretpass \
  --workspace-name "Demo" --workspace-slug demo
uvicorn app.main:app --reload

requires-python >=3.12. Earlier versions won't install the mcp package.

Fly.io / Render / Kubernetes

All of these run the same Dockerfile. You supply:

  • DATABASE_URL pointing at a reachable Postgres 13+
  • SECRET_KEY
  • A writable volume at /app/data if using local file storage
  • Egress to your memory providers / webhook subscribers

Nakatomi doesn't need much — a single web process + Postgres is enough for most workloads.

Scaling notes

  • One process is fine until 50+ writes/sec. Above that, run multiple app replicas behind a load balancer; the webhook worker uses SELECT ... FOR UPDATE SKIP LOCKED so they don't step on each other.
  • Postgres sizing. Nakatomi writes 1-3 rows per CRM mutation (the entity + a timeline event + sometimes an audit row + sometimes a webhook delivery). Budget 100-300 row-writes per agent action.
  • File storage. Local volume is fine for low volume. Switch to S3 (or R2) when you cross ~100GB or need cross-region access.
  • Rate limiting. Protects you from a single agent. See Rate-Limiting.

Monitoring

Nakatomi logs to stdout. Pipe it into whatever you use (Loki, Datadog, OpenTelemetry — OTEL support is on the roadmap). Key signals:

  • /health should return 200 in under 100ms
  • Worker log lines like webhook worker started on boot, webhook worker iteration failed on errors
  • 429s in access logs → rate limit is firing
  • 5xx rate → look at recent migrations or Postgres connection pool

Backups

Two layers:

  1. Postgres — whatever your host provides. Railway takes automated snapshots; Fly has fly postgres backup; bare VPS → pg_dump on cron.
  2. WorkspaceGET /export on a schedule, ship to S3. Lets you restore one workspace without a full DB restore. See Export-Import.

Upgrading

  1. Pull the new image / tag.
  2. The container runs alembic upgrade head on startup.
  3. If CI is green on main, the migration is safe.

Rollbacks: Alembic downgrade -1 works for most migrations; file a GitHub issue if one doesn't.

Clone this wiki locally