---
title: "Deployment"
description: "Run mikan under PM2 with persistent state, managed sandbox images, graceful shutdown, and an optional health endpoint."
url: "https://geminixiang.github.io/deployment/"
---

# Deployment

PM2 daemonizes mikan, restarts it after failures, retains stdout/stderr logs, and can start it
    on boot.
    The mikan host process uses Docker to create per-conversation tool containers from the selected
    sandbox image.
    Keep one host-only state directory for settings, the office registry, vaults, per-office state,
    and GitHub polling watermarks.

## Prerequisites

- 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.

## PM2 deployment

1. Install mikan and PM2:

   ```bash
   npm i -g @geminixiang/mikan pm2
   ```

2. Create the global settings file in the exact state directory the service will use:

   ```bash
   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:

   ```bash
   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:

   ```bash
   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:

   ```bash
   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):

   ```js
   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:

   ```bash
   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.

## Upgrade

```bash
npm i -g @geminixiang/mikan
pm2 reload mikan
```

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

```bash
pm2 reload ecosystem.config.cjs
```

  The included PM2 configuration waits up to 60 seconds after SIGTERM before SIGKILL. mikan first
  drains the conversation runtime, stops the events watcher, and gives Sentry up to five seconds to
  flush. Runs that exceed the supervisor timeout can still be terminated.

### 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:

```bash
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 migration renames directories under both the workspace and the state directory. Take a copy of
  `<state-dir>` (and ideally the workspace) before the first start on the new version.

## Health endpoint

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

```bash
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](https://github.com/geminixiang/mikan/blob/main/deploy/pm2/ecosystem.config.cjs) and [Sandbox modes](/sandbox/) for mode-specific requirements.
