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.ymland.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:
MOIRA_FILES=https://raw.githubusercontent.com/moira-mcp/moira/mastercurl -fLO "$MOIRA_FILES/docker-compose.yml"curl -fLo .env.example "$MOIRA_FILES/.env.example"-
Create your config
Terminal window cp .env.example .envFor a
localhostrun, the defaults work unchanged. -
Start the container
Terminal window docker compose up -dCompose pulls
ghcr.io/moira-mcp/moira:latest, the current public release, and starts the container. -
Open the Web UI
http://localhost:8080Your instance also serves this documentation at
http://localhost:8080/docs/(and/ru/docs/), and the MCP endpoint athttp://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:
EMAIL_PROVIDER=smtpEMAIL_FROM=moira@example.comSMTP_HOST=smtp.example.comSMTP_PORT=587SMTP_SECURE=falseSMTP_REQUIRE_TLS=trueSMTP_USER=your-smtp-userSMTP_PASSWORD=your-smtp-passwordSMTP 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:
read -s ADMIN_PASSWORDexport ADMIN_PASSWORDdocker compose exec -e ADMIN_PASSWORD moira npx tsx scripts/create-admin-user.tsunset ADMIN_PASSWORDADMIN_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:
docker compose exec moira npm run prepare:account-approval-downgrade -- \ --confirm-block-pending-usersThe 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.
| Variable | Default | Purpose |
|---|---|---|
MOIRA_HOST | localhost:8080 | Public host (protocol auto-detected) |
MOIRA_PORT | 8080 | Host port mapped to the container |
STATIC_ARTIFACTS_DOMAIN | static.localhost:8080 | Domain for served HTML artifacts |
For a real host or a different port, edit all three together:
MOIRA_HOST=moira.example.comMOIRA_PORT=8080STATIC_ARTIFACTS_DOMAIN=static.example.comcaution:
STATIC_ARTIFACTS_DOMAINis 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:
| Variable | Generated value |
|---|---|
BETTER_AUTH_SECRET | Session encryption key |
TELEGRAM_ENCRYPTION_KEY | Telegram credential encryption key |
ADMIN_PASSWORD | Admin password, printed to the logs once |
The admin login is shown once in the container logs on first start:
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:
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/callbackWORKSPACE_GITHUB_APP_INSTALL_URL=https://github.com/apps/<github-app-slug>/installations/newWORKSPACE_CREDENTIAL_VAULT_KEY=<64-hex-random-key>WORKSPACE_CREDENTIAL_VAULT_KEY_VERSION=v1Generate the dedicated vault key outside the repository and put its output in the untracked deployment environment:
openssl rand -hex 32docker compose up -dThe 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:
docker compose --profile workspaces up -dWith 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/mcpAdd 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:
docker compose pulldocker compose up -ddocker compose psIf an existing .env came from the older broken template and still contains the removed 0.3.5
tag, change it once before updating:
MOIRA_IMAGE=ghcr.io/moira-mcp/moira:latestBefore 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:
docker compose logs moiradocker 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 moiradocker compose pslatest 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.
docker compose run --rm moira npm run reconcile -- statusdocker 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 -- applydocker compose up -dFor 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.
RELEASE_VERSION=x.y.zTARGET_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:
cp .env.example .envmkdir -p extensionscp -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:
printf '\nMOIRA_EXTENSION_RUNNER_URL=http://moira-extension-runner:9110\n' >> .envdocker compose --profile extensions up -d --buildThe 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:
docker compose --profile extensions up -d --force-recreate --wait moira-extension-runnerdocker compose --profile extensions restart moiraInspect rejected bundles and the live catalogue without exposing the runner port:
docker compose --profile extensions logs moira-extension-runnerdocker compose --profile extensions exec moira-extension-runner curl -fsS http://127.0.0.1:9110/healthAn 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):
WORKFLOWS_DIRS=./workflows/production:./my-private-workflows/productionThe 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:
docker compose up -d --buildRelated
- Quick Start - Connect an AI client
- MCP Clients - Client integrations