Skip to content

Self-Hosting

Run Moira on your own machine with Docker Compose. The default path pulls a prebuilt image from the registry — no source checkout or local build needed.

Prerequisites

  • Docker with the Compose plugin (docker compose)
  • The public docker-compose.yml and .env.example; no source build, private repository, host SQLite CLI, or separate upgrade script is required for the normal path

Quick Start

Download the current public self-host files:

Terminal window
MOIRA_FILES=https://raw.githubusercontent.com/moira-mcp/moira/master
curl -fLO "$MOIRA_FILES/docker-compose.yml"
curl -fLo .env.example "$MOIRA_FILES/.env.example"
  1. Create your config

    Terminal window
    cp .env.example .env

    For a localhost run, the defaults work unchanged.

  2. Start the container

    Terminal window
    docker compose up -d

    Compose pulls ghcr.io/moira-mcp/moira:latest, the current public release, and starts the container.

  3. Open the Web UI

    http://localhost:8080

    Your instance also serves this documentation at http://localhost:8080/docs/ (and /ru/docs/), and the MCP endpoint at http://localhost:8080/mcp.

On first start, Moira generates the missing secrets and a one-time admin password.

Configuration

DEPLOYMENT_MODE=self-host (the default) runs a private-team install with open registration and administrator approval. A newly registered account can inspect its approval status and sign out, but cannot use workflows, API tokens, OAuth, or MCP until an administrator approves it. Email verification is a separate gate and is not required in self-host mode. Missing secrets are generated on first start.

The self-host administrator keeps the Users page for approval, blocking, and account recovery. Cross-user workflow/execution/artifact administration, cloud analytics, the operational dashboard, and deliberate monitoring-test tools are disabled by the server and omitted from the navigation. These are deployment capabilities, not security controls implemented only in the UI. The SaaS policy enables them through the same resolver used by the API and the Web UI. The self-host dashboard still shows database health, setting-definition status, and managed-workflow reconciliation, but it neither requests nor renders installation-wide workflow or execution totals.

Approve new accounts

In the Web UI, sign in as an administrator, open Admin Panel → Users, and select an account marked Pending approval. Choose Approve account and confirm the dialog. The action shows progress while it is running and changes the account badge to Approved; the registrant’s waiting page detects that transition and opens Moira without another login. On a narrow screen, open the Admin Panel navigation with the menu button first.

The equivalent API operation is POST /api/admin/users/:id/approve; GET /api/admin/users lists the approval timestamp. Approval is idempotent, so retrying a request after an uncertain response does not replace the first approval time or create a second transition. Existing users are marked approved during the database migration; the bootstrap administrator is always created as approved. Blocking and email verification remain independent controls: a blocked account is denied even when approved.

Configure email or recover without it

Email delivery is disabled by default. To enable password-reset and verification messages, configure a standard SMTP server in .env and restart the container:

Terminal window
EMAIL_PROVIDER=smtp
EMAIL_FROM=moira@example.com
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_REQUIRE_TLS=true
SMTP_USER=your-smtp-user
SMTP_PASSWORD=your-smtp-password

SMTP authentication is optional, but SMTP_USER and SMTP_PASSWORD must be set together. Use SMTP_SECURE=true for implicit TLS (commonly port 465); otherwise SMTP_REQUIRE_TLS=true requires STARTTLS. Brevo remains available with EMAIL_PROVIDER=brevo, BREVO_API_KEY, and EMAIL_FROM. Invalid partial configuration stops startup. SaaS mode also refuses to start without a real provider.

With EMAIL_PROVIDER=none (the default), Moira starts normally and the Web UI explains that forgot-password and email actions are unavailable. An administrator can instead open an ordinary user’s detail page, choose Set temporary password, and send that password through a separate secure channel. This action revokes all of the user’s sessions, API tokens, OAuth credentials and consents. The user signs in with the temporary password and must immediately replace it. Administrator accounts must use the command-line recovery procedure below.

EMAIL_PROVIDER=test is only a log sink for automated testing. It never means that delivery is configured and never reports a logged message as sent.

Recover administrator access

From the directory containing docker-compose.yml, supply a new password without putting it in shell history:

Terminal window
read -s ADMIN_PASSWORD
export ADMIN_PASSWORD
docker compose exec -e ADMIN_PASSWORD moira npx tsx scripts/create-admin-user.ts
unset ADMIN_PASSWORD

ADMIN_PASSWORD is required. ADMIN_EMAIL and ADMIN_ID default to admin@moira.local and system-admin; DB_PATH defaults to ./data/moira.db. Add -e ADMIN_EMAIL, -e ADMIN_ID, or -e DB_PATH to docker compose exec when overriding them. The command creates or repairs the administrator, marks it verified and approved, clears any account block, and replaces its credential. It never prints the password.

Roll back to a version without account approval

An older image ignores approvedAt. Before pinning such an image, stop external traffic, back up the database, and run the conversion on the current image:

Terminal window
docker compose exec moira npm run prepare:account-approval-downgrade -- \
--confirm-block-pending-users

The command refuses to run without the confirmation argument. It blocks every pending user with the legacy blocked control and revokes that user’s sessions, API tokens, OAuth tokens, and OAuth consents in one transaction. Verify the printed counts before stopping the current container. Do not roll back first, because the old image cannot make this conversion. If you later restore the approval-aware version, review each converted account before approving and explicitly unblocking it.

Host-dependent variables

These three must point at your host and keep their ports consistent. The shipped defaults target localhost:8080, so a localhost run needs no edits.

VariableDefaultPurpose
MOIRA_HOSTlocalhost:8080Public host (protocol auto-detected)
MOIRA_PORT8080Host port mapped to the container
STATIC_ARTIFACTS_DOMAINstatic.localhost:8080Domain for served HTML artifacts

For a real host or a different port, edit all three together:

Terminal window
MOIRA_HOST=moira.example.com
MOIRA_PORT=8080
STATIC_ARTIFACTS_DOMAIN=static.example.com

caution: STATIC_ARTIFACTS_DOMAIN is required — startup aborts if it is empty.

Auto-generated secrets

In self-host mode these are generated on first start and persisted to <data-dir>/.secrets.env. Leave them empty in .env:

VariableGenerated value
BETTER_AUTH_SECRETSession encryption key
TELEGRAM_ENCRYPTION_KEYTelegram credential encryption key
ADMIN_PASSWORDAdmin password, printed to the logs once

The admin login is shown once in the container logs on first start:

Terminal window
docker compose logs | grep -A3 "ADMIN LOGIN"

Sign in with ADMIN_EMAIL (default admin@moira.local) and the printed password.

Enable the GitHub workspace connection

The GitHub workspace connection is separate from GitHub social login. It stays disabled unless all GitHub App and credential-vault values are present and valid. Create a GitHub App with expiring user authorization tokens and “Request user authorization (OAuth) during installation” enabled, grant it the repository permissions Codespaces (write), Codespaces lifecycle admin (write), Codespaces metadata (read), Contents (read) and Metadata (read), then configure its callback URL to the exact Moira API path and its installation URL to the app’s GitHub slug:

Terminal window
WORKSPACE_GITHUB_APP_CLIENT_ID=<github-app-client-id>
WORKSPACE_GITHUB_APP_CLIENT_SECRET=<github-app-client-secret>
WORKSPACE_GITHUB_APP_CALLBACK_URL=https://moira.example.com/api/integrations/github/callback
WORKSPACE_GITHUB_APP_INSTALL_URL=https://github.com/apps/<github-app-slug>/installations/new
WORKSPACE_CREDENTIAL_VAULT_KEY=<64-hex-random-key>
WORKSPACE_CREDENTIAL_VAULT_KEY_VERSION=v1

Generate the dedicated vault key outside the repository and put its output in the untracked deployment environment:

Terminal window
openssl rand -hex 32
docker compose up -d

The callback origin must equal the public Moira origin. A public callback uses HTTPS and has no query or fragment. Use the client secret generated by GitHub; placeholder, repeated-character and other low-diversity values are rejected. The vault key is not auto-generated and must remain stable across container restarts; changing it makes existing connection credentials unreadable.

If a key or ciphertext is lost, ordinary Reconnect and Disconnect cannot prove remote revocation. First remove the Moira GitHub App grant in GitHub settings. Then return to Settings → Integrations → GitHub and choose Forget after external revoke. The confirmation deletes unreadable local ciphertext; do not confirm while GitHub still lists the grant. If the original key and version are restored, Moira no longer offers this unreadable-credential recovery. Ordinary Reconnect GitHub and Disconnect are available again; both revoke the readable predecessor credential exactly.

The same external-revoke confirmation appears if a refresh may have returned a new credential that Moira could neither retain nor revoke. Revoke the entire GitHub App grant before confirming; this state deliberately disables Reconnect and ordinary Disconnect.

After the container is healthy, each user opens Settings → Integrations → GitHub, selects Connect GitHub, completes the browser authorization and installs the app for the intended personal repositories. Authorization never happens through an MCP tool or agent. When setup is missing or a credential must be renewed, the user returns to this website.

Workspace creation and agent operations additionally require WORKSPACE_CODESPACES_ENABLED=true and the connector pair from the disabled-by-default Compose profile:

Terminal window
docker compose --profile workspaces up -d

With the connection and connector in place, an authenticated MCP client uses the one workspace tool, choosing the operation with action: list shows approved repositories and existing workspaces, create provisions a persistent personal-billed Codespace for an approved repository, and exec, stat, search, read, write, apply_patch, upload and download work inside it by workspace_id. stop keeps the repository data; delete removes the Codespace and requires explicit confirmation. Different chats and clients may reuse the same workspace; nothing is deleted when a command finishes or a client disconnects.

An agent working in a workspace acts as the ordinary Codespace user: it can read the repository, use the network and read any secrets configured for that Codespace. Moira’s isolation protects the Moira server and other users, not the workspace from the agent its owner authorized. When setup is incomplete, the tools return a safe error with a link to this Settings page instead of starting any authorization flow.

Users manage the same workspaces from Settings → Integrations → Cloud workspaces: create one for an approved repository, start or stop it (stop keeps the repository data) and delete it after an explicit confirmation. Administrators open Admin → Settings → Workspaces to see the instance readiness (configuration, connector, reconciliation backlog, active workspaces and operations against their limits) and to pause work with the global or provider kill switch; pausing refuses new workspaces, starts and agent operations and stops running workspaces without deleting anything.

For monitoring, GET /api/health and the MCP /health endpoint report the readiness state (disabled, misconfigured, control_disabled, connector_unavailable or ready); a disabled feature is healthy, while invalid configuration or an unreachable connector marks the instance degraded. Health answers from a cached decision refreshed on the reconciliation interval, and the connector probe is bounded to two seconds, so a stalled connector cannot hang the health check. The internal metrics port exposes moira_workspace_* gauges and counters. Alert when moira_workspace_ready stays at 0 with WORKSPACE_CODESPACES_ENABLED=true, when moira_workspace_connector_available is 0, when moira_workspace_reconciliation_oldest_due_age_seconds exceeds several reconcile intervals, or when moira_workspace_rejections_total grows for quota or busy codes.

Disconnect disables local use before GitHub revocation. If GitHub is temporarily unavailable, the page shows a revocation-pending state and Disconnect retries the exact encrypted capability; the credential is not returned to the browser or model.

Connect an MCP Client

The MCP endpoint is your host plus /mcp:

http://localhost:8080/mcp

Add it as an MCP server in your AI client and complete OAuth authentication. See Quick Start for client configuration.

Updating and Recovery

The ordinary update path does not require a version lookup or a repository-side helper:

Terminal window
docker compose pull
docker compose up -d
docker compose ps

If an existing .env came from the older broken template and still contains the removed 0.3.5 tag, change it once before updating:

Terminal window
MOIRA_IMAGE=ghcr.io/moira-mcp/moira:latest

Before the new self-host image runs migrations against an existing database, its startup guard uses SQLite online backup and verifies PRAGMA integrity_check. It stores the database and matching prompt-manifest.json under data/.moira-startup-backups/current/, rotating the prior states to previous-1/ and previous-2/. A genuine first start has no database to back up.

The current slot carries a persistent initialization-pending marker. If the container or host is killed before initialization commits, the next start restores that verified slot before creating a new backup, so a partially migrated database cannot become the next baseline. On the first start, a marker without a fake database backup records that no database existed. An interrupted retry removes only the incomplete new database, WAL/SHM, and prompt manifest before starting clean; successful initialization removes the marker.

If schema, prompt, or workflow initialization fails after writing data, the guard removes WAL/SHM, restores the verified database and prompt manifest, writes /tmp/init-failed, and keeps MCP, the API, and nginx stopped. The recovery copy remains available. Inspect and retry after correcting the configuration or catalog:

Terminal window
docker compose logs moira
docker compose exec -T moira sqlite3 /app/data/moira.db 'PRAGMA integrity_check;'
docker compose exec -T moira sqlite3 /app/data/.moira-startup-backups/current/moira.db 'PRAGMA integrity_check;'
docker compose restart moira
docker compose ps

latest intentionally follows the current public release. Automatic recovery protects persistent data; it cannot replace the Docker image itself. If the image cannot reach the startup guard, the database has not been migrated. Temporarily set MOIRA_IMAGE to a previous version from GitHub Releases, run docker compose up -d, and return to latest after a corrected release.

An upgrade that changes a bundled workflow can leave a run that was paused inside it unable to continue. Startup names those runs before applying the update — the workflow, the execution and the node each is paused on — so they appear in docker compose logs moira while the old definitions are still in place. The warning never stops an upgrade. A run invalidated because its workflow changed can be repaired afterwards with session({ action: 'diagnose', ... }) followed by session({ action: 'recover', ... }); a run whose workflow the upgrade removes cannot, since no definition remains to resume against, and the line says which case it is. No warning means no paused run would be affected.

When conflict detection has produced a local bundle under data/.moira-reconciliation/pending, follow its AGENT INSTRUCTIONS with one-off Compose CLI containers. The CLI itself does not stop or restart services and does not replace the database snapshot. It uses only local files—never --force, MCP, an HTTP API, or UI transport. Initialization fails closed: after restoring the database the container stops with MCP, API, and nginx unavailable. After bundle application, the final docker compose up -d starts that stopped container normally.

Terminal window
docker compose run --rm moira npm run reconcile -- status
docker compose run --rm moira npm run reconcile -- diff --reference owner/slug
# Read previous.json, current.json, and incoming.json from the printed bundle paths.
# Record one revision-bound current, incoming, or merged decision for every conflict:
docker compose run --rm moira npm run reconcile -- choose \
--reference owner/slug --selection incoming --revision REVISION \
--rationale "Incoming supersedes the local experiment"
docker compose run --rm moira npm run reconcile -- apply
docker compose up -d

For a merge, use incoming.json as the base, reapply only local intent still justified by previous.json → current.json, validate the complete merged state with reconcile validate, and pass it to choose --selection merged --file .... choose changes only the local decisions manifest. apply refuses an incomplete or stale manifest and is the only command that changes the database.

Optional preflight before downtime

Operators who want to test an exact image on an isolated database copy before replacing the active container may download the advanced helper from that release. This is optional; normal updates use the two Compose commands above. The host sqlite3 CLI is required only for this advanced path.

Terminal window
RELEASE_VERSION=x.y.z
TARGET_IMAGE=ghcr.io/moira-mcp/moira:${RELEASE_VERSION}
curl -fLo self-host-upgrade.sh "https://raw.githubusercontent.com/moira-mcp/moira/v${RELEASE_VERSION}/scripts/self-host-upgrade.sh"
chmod +x self-host-upgrade.sh
./self-host-upgrade.sh preflight "$TARGET_IMAGE"
./self-host-upgrade.sh upgrade "$TARGET_IMAGE"

The helper retains its verified snapshot and diagnostic copy under .moira-upgrade/; use ./self-host-upgrade.sh rollback if its replacement or health check fails. Because this advanced path writes the exact image to .env, set MOIRA_IMAGE=ghcr.io/moira-mcp/moira:latest afterward if you want to rejoin the normal release channel.

Enable extensions

Extensions require a source checkout because the companion runner image is built locally. The published Moira application image remains unchanged. From the repository root:

Terminal window
cp .env.example .env
mkdir -p extensions
cp -R examples/extensions/webhook-notify extensions/

Replace the placeholder example.com in both the copied manifest’s top-level permissions.network and its communication channel’s permissions.network with the exact host your endpoint uses. Node and channel permissions are separate. Set the endpoint, default notification recipient, and other values for each user on Moira’s Settings page. Then add the runner address to .env and enable the profile:

Terminal window
printf '\nMOIRA_EXTENSION_RUNNER_URL=http://moira-extension-runner:9110\n' >> .env
docker compose --profile extensions up -d --build

The profile builds the runner, mounts ./extensions read-only into that container, waits for runner health, and then starts Moira. The runner is absent from an ordinary docker compose up -d.

Runner and Moira load the extension catalogue at startup. After changing installed bundles:

Terminal window
docker compose --profile extensions up -d --force-recreate --wait moira-extension-runner
docker compose --profile extensions restart moira

Inspect rejected bundles and the live catalogue without exposing the runner port:

Terminal window
docker compose --profile extensions logs moira-extension-runner
docker compose --profile extensions exec moira-extension-runner curl -fsS http://127.0.0.1:9110/health

An empty directory is valid. The health response lists loaded node types and communication channel IDs. If Moira reports the registry unavailable, confirm the URL, runner health and logs, then restart both services in the order above. If one contribution is missing, inspect the manifest rejection reasons. See Writing an Extension for the manifest, SDK, permissions, settings, editor, delivery, failure and security contracts.

Adding Your Own Workflow Flows

The image ships a bundled workflow catalog in ./workflows/production. To ALSO load your own flows, set WORKFLOWS_DIRS to a colon-separated list of catalog base directories (each containing a flows/<uuid>.json layout):

Terminal window
WORKFLOWS_DIRS=./workflows/production:./my-private-workflows/production

The directories are merged and de-duplicated by (owner, slug). A later directory overrides an earlier one on a collision, so a directory listed last can extend or shadow the bundled catalog. Unset → just the bundled ./workflows/production. Mount your extra directory into the container (e.g. via a compose volume) so the path exists at runtime.

Build From Source

Building locally is an alternative for contributors who need to modify the image. In docker-compose.yml, comment out the image: line and uncomment the build: block, then:

Terminal window
docker compose up -d --build