Skip to content

Deployment

Docker Compose

The full stack runs with a single command:

bash
docker compose up -d

This starts:

  • doto-postgres — PostgreSQL 16 with a named volume for persistence
  • doto-server — The API server plus the built web frontend (waits for Postgres to be healthy)

The server runs database migrations automatically on startup.

Environment variables

Copy .env.example to .env and configure:

bash
# Required
DATABASE_URL=postgresql://doto:doto_dev@postgres:5432/doto_dev
PORT=3000
NODE_ENV=production

# Optional — AI classification
AI_PROVIDER=openai          # openai or anthropic
AI_MODEL=gpt-4o-mini        # optional, auto-selects if omitted
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_BASE_URL=...         # optional, override the OpenAI endpoint
ANTHROPIC_BASE_URL=...      # optional, override the Anthropic endpoint

In Docker Compose, DATABASE_URL uses the service name postgres as the host. For local development outside Docker, use 127.0.0.1.

Build the image

bash
docker compose build

The Dockerfile uses a two-stage build:

Stage 1 — builder (oven/bun:1.3.12)

  1. Install all dependencies (including devDependencies)
  2. Build the React frontend from packages/web
  3. Run prisma generate
  4. Compile the server TypeScript with tsc

Stage 2 — production (oven/bun:1.3.12)

  1. Install production dependencies only
  2. Copy compiled server dist/, prisma/, prisma.config.ts, and built web assets from builder
  3. Run prisma generate again (ensures ESM-compatible client)
  4. Set WEB_DIST_PATH so the server can serve the frontend
  5. Set entrypoint

Startup sequence

packages/server/scripts/entrypoint.sh:

bash
bun run prisma migrate deploy   # Apply any pending migrations
bun /app/packages/server/dist/index.js   # Start the server

Health check

Docker Compose polls the health endpoint every 10 seconds:

GET http://localhost:3000/health

The server is considered healthy when it returns 200. The health endpoint also checks database connectivity — it returns 503 if the database is unreachable.

Frontend routing

The production deployment is same-origin:

  • API requests are served from http://localhost:3000/api/v1/*
  • The login UI and other SPA routes are served from http://localhost:3000/*
  • Google OAuth callback redirects back to /auth/callback on the same origin unless FRONTEND_URL is explicitly set

Data persistence

PostgreSQL data is stored in a named Docker volume doto-pg-data. It survives container restarts and docker compose down.

To wipe all data:

bash
docker compose down -v

Logs

bash
docker compose logs server    # API server logs
docker compose logs postgres  # Database logs
docker compose logs -f        # Follow all logs

Released under the MIT License.