Skip to content

Deployment

Run mikan under PM2 with persistent state, managed sandbox images, graceful shutdown, and an optional health endpoint.

PM2 supervisor

PM2 daemonizes mikan, restarts it after failures, retains stdout/stderr logs, and can start it on boot.

Managed image sandbox

The mikan host process uses Docker to create per-conversation tool containers from the selected sandbox image.

Persistent state

Keep one host-only state directory for settings, the office registry, vaults, per-office state, and GitHub polling watermarks.

  • Node.js >=22.19.0
  • at least one complete Slack, Telegram, Discord, or GitHub credential set
  • a persistent state directory (default ~/.mikan); the working directory defaults to <state-dir>/workspace
  • Docker available to the PM2 user when using image:* or container:*

The published mikan-sandbox image is a tool runtime, not a standalone mikan server image. PM2 runs mikan on the host; image mode creates and manages containers through the host Docker daemon.

  1. Install mikan and PM2:

    Terminal window
    npm i -g @geminixiang/mikan pm2
  2. Create the global settings file in the exact state directory the service will use:

    Terminal window
    mkdir -p /srv/mikan/workspace
    mikan --onboard --state-dir=/srv/mikan/state

    Review /srv/mikan/state/settings.json. The directory must be owned by the PM2 process user and must not be world-writable; chmod 0700 /srv/mikan/state is recommended.

  3. Create the secrets file. All platform tokens, provider keys, and other secrets live in ~/.mikan/mikan.env — mode 0600, outside any repo tree — and the ecosystem file loads it at start. Never put secrets in the ecosystem file or PM2’s own environment:

    Terminal window
    curl -o ~/.mikan/mikan.env https://raw.githubusercontent.com/geminixiang/mikan/main/deploy/pm2/mikan.env.example
    chmod 600 ~/.mikan/mikan.env

    Fill in at least one complete platform credential set plus an LLM provider key. Run mikan env to see the full annotated inventory and what is currently set.

  4. Pull the managed sandbox image:

    Terminal window
    docker pull ghcr.io/geminixiang/mikan-sandbox:latest

    Do not also start a long-lived container when the selected mode is image:*. A pre-existing container is needed only for container:<name> mode.

  5. Download the ecosystem file:

    Terminal window
    curl -O https://raw.githubusercontent.com/geminixiang/mikan/main/deploy/pm2/ecosystem.config.cjs

    The ecosystem file is supervision-only: process name, restart policy, graceful-shutdown timeout, and the mikan.env loader. Behavior (model, sandbox limits, reply modes) stays in <state-dir>/settings.json.

  6. Edit args to use absolute paths and one sandbox mode (run mikan --help for the flag reference):

    args: "--state-dir=/srv/mikan/state --sandbox=image:ghcr.io/geminixiang/mikan-sandbox:latest /srv/mikan/workspace",

    The working-directory argument is optional; without it mikan uses <state-dir>/workspace and creates it on first start.

  7. Start and persist the process:

    Terminal window
    pm2 start ecosystem.config.cjs
    pm2 save
    pm2 startup
  8. Run the command printed by pm2 startup to enable boot startup.

Relative workspace paths are resolved from PM2’s current working directory. Absolute paths avoid silently opening a different workspace after service migration or reboot.

Terminal window
npm i -g @geminixiang/mikan
pm2 reload mikan

After editing ~/.mikan/mikan.env, reload through the ecosystem file so the environment is re-read:

Terminal window
pm2 reload ecosystem.config.cjs

Upgrading across the office layout migration

Section titled “Upgrading across the office layout migration”

Conversation directories are named by office key (v1-<platform>-…) rather than by raw platform id. Upgrading from a release that used raw ids runs a migration at startup: legacy workspace directories, conversation vaults, and per-conversation state trees are renamed into the office-key layout, journaled in <state-dir>/office-registry.json so an interrupted run resumes on the next start. Managed image:* containers keep their writable layers across the rename — a container still mounting legacy paths is snapshotted, removed, and re-created with translated mounts before its next message, not during boot.

Boot fails rather than continuing when a legacy directory cannot be attributed — after the layout change such a directory is invisible to the runtime, and starting anyway would present the conversation as silently empty. The startup error names each unresolved directory.

Ownership is verified, not guessed: a directory is claimed automatically only when exactly one enabled platform’s id format could have produced its name. A directory whose name is ambiguous across two enabled platforms — bare digits, with both Telegram and Discord running — needs an explicit owner:

Terminal window
pm2 stop mikan
mikan office list --state-dir=/srv/mikan/state --workspace=/srv/mikan/workspace
mikan office claim 100200300 telegram --state-dir=/srv/mikan/state --workspace=/srv/mikan/workspace
pm2 start mikan

claim only records the owner; the daemon performs the move on its next start, which is why it is run with the process stopped. Vault or state-directory conflicts — the same conversation present under both the legacy and the office-key name — are reported the same way and must be merged by hand under <state-dir>.

The formal Web adapter requires the portal HTTP server plus at least one complete, dedicated account-sign-in OAuth client:

Terminal window
LINK_URL=https://mikan.example.com
LINK_PORT=8181
WEB_GITHUB_OAUTH_CLIENT_ID=...
WEB_GITHUB_OAUTH_CLIENT_SECRET=...
WEB_GOOGLE_OAUTH_CLIENT_ID=...
WEB_GOOGLE_OAUTH_CLIENT_SECRET=...

Register these exact callbacks with the providers:

https://mikan.example.com/auth/github/callback
https://mikan.example.com/auth/google/callback

LINK_URL is the canonical public origin. Use HTTPS in production: account callback URLs are derived from this configured value, not arbitrary Host or forwarded headers, and browser-session cookies are marked Secure. Only explicit loopback development may omit LINK_URL, derive the origin from a loopback request, and use a non-Secure cookie.

Account OAuth apps are separate from the broader /pi-login credential-onboarding apps. Provider tokens are discarded after profile lookup; browser login does not put provider credentials in a vault. The account/workspace registry is restart-safe at <state-dir>/web/registry.json, so include it in state-directory backups.

The published npm package includes the formal React product in dist/web-app. When Web account OAuth is configured, mikan serves it at / and uses client-side /w/<opaque-workspace-id> routes. Hashed assets are immutable; application HTML is not cached. API, auth, /admin, /pi-session, and /pi-login routes keep precedence over the SPA fallback. Put the same HTTPS origin in front of the UI, API, auth callbacks, and workspace event stream—no separate cross-origin frontend is required.

The portal server exists only when LINK_PORT is set, or when LINK_URL causes the default port 8181 to be used:

Terminal window
curl http://127.0.0.1:8181/health
# {"ok":true}

/health is a liveness check for the HTTP process only. It does not verify platform connections, Docker, sandbox provisioning, LLM providers, or event delivery, so do not use it as the only readiness signal.

See the maintained PM2 ecosystem file on GitHub and Sandbox modes for mode-specific requirements.