This guide covers first boot, production hardening, upgrades, and the main operator tasks for a self-hosted Deft workspace.
Self-hosted Deft is a single-workspace deployment. You run the PostgreSQL database and application stack yourself. Workspace storage is on your infrastructure; configured AI providers and external clients can receive workspace content as described under AI data flow.
Each deployment supports one organisation. The first user to sign up becomes the workspace owner; everyone else joins through invite links generated from Settings -> Members. This one-workspace boundary is the supported v1 product contract, not a license restriction. AGPL-3.0-only permits network use under its terms, including the Corresponding Source obligation for modified versions.
AI is bring-your-own-provider. Deft can use Anthropic, OpenAI, OpenRouter, or a local Ollama server, but the core workspace works without any AI provider.
Background and scheduled work runs through Deft's PostgreSQL-backed job queue; Redis and BullMQ are not runtime dependencies. See the job queue architecture decision.
| Requirement | Notes |
|---|---|
| Docker Desktop or Docker Engine | A running Linux container engine and Compose v2 |
| openssl | Used to generate secrets; ships with macOS, Linux, Git for Windows |
| AI provider | Optional; configure later from Settings -> AI |
Start with 2 vCPU / 4 GB RAM for a small evaluation and measure resource use on your workload. This is a starting configuration, not a capacity guarantee. Commands below use Bash; Windows users can use Git Bash with Docker Desktop.
For the independently sideloaded Email developer preview, use Host
v0.3.0-preview.123 and the apps-developer-preview browser profile listed
in its browser-feature-evidence.json. Follow the independent
Email setup guide
and its compatibility manifest
for the exact image digest, schema 120, sealed Kit downloads, HTTPS/OAuth and
Runtime configuration. Host preview 122 includes the session-refresh pool fix
and production JWT-secret validation, retaining schema 120 without a new migration.
This path requires an operator; it is not the stable
core quick start below. Feature flags beginning NEXT_PUBLIC_ are compiled
into the browser bundle. Setting them only when a container starts does not
enable Apps, resource sync or attachments missing from that build. Server gates,
purpose-separated keys, reviewed grants and the independent Runtime are also
required. Keep only one active Host when operating the small demo database;
overlapping Host processes consume separate connection pools.
For a fresh evaluation, follow the README quick start,
which pins v0.3.0-preview.15 and its image digest. Use that release's own Compose
files and environment template together; this source checkout can differ from
the published image. The source-build instructions below are for contributors.
Tagged preview releases publish an amd64 image to
ghcr.io/maneek21/deft. The release image injects NEXT_PUBLIC_APP_URL,
NEXT_PUBLIC_API_URL, and NEXT_PUBLIC_WS_URL when the container starts, so
the same image works on localhost or a custom domain.
For a named release, download docker-compose.yml, compose.prod.yml,
compose.release.yml, default.env.example, self-hosting.md,
release-manifest.json, and SHA256SUMS from the selected GitHub release into
a new directory. Copy
default.env.example to .env. Generate separate values with openssl rand -hex 32
for all four required secrets: POSTGRES_PASSWORD, JWT_SECRET,
JWT_REFRESH_SECRET, and ENCRYPTION_KEY. Production startup requires distinct
JWT signing keys of at least 32 characters, rejects known template/default values
and surrounding whitespace, and preserves accepted key bytes exactly. Docker
runs in production mode even on localhost. Set the image in .env (the tag has no leading v):
DEFT_IMAGE=ghcr.io/maneek21/deft:0.3.0-preview.15For another host or port, set NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_API_URL,
and NEXT_PUBLIC_WS_URL to the browser-reachable URLs first. Keep their defaults
for localhost ports 3000/3001. Leave provider keys empty for a basic evaluation.
Host v0.3.0-preview.123 pins the reviewed PostgreSQL 16.15 / pgvector 0.8.6 image in docker-compose.yml. Keep that immutable database image with this preview's deployment assets: fresh initialization and upgrade lineage checks include PostgreSQL and extension versions, so a floating pg16 tag can produce an unrecognized catalog.
Use the release overlay in every application/tool command:
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml pull
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml up -d postgres
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml run --rm init
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml up -d deft
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml run --rm doctor
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml run --rm smokeRelease assets include SHA256SUMS, an SPDX SBOM, and a manifest containing
the exact commit, image digest, keyless-signing identity, provenance type, and
upgrade baseline. New deft.release.v2 manifests explicitly identify
release_scope as core or hermes-certified. Core releases contain no Hermes
bundle or certification claims. Historical deft.release.v1 manifests retain
their original release-specific Hermes evidence. Hermes-certified releases include
hermes-employee-release-gate.json and
deft-hermes-integration-<version>.tar.gz. The release manifest binds their
SHA-256 digests, the bundle manifest and content digests, compatibility range,
and exact tested Hermes runtime. Release-tagged images are signed by the release
workflow and carry GitHub build provenance. Verify checksums and the image
digest before deploying or installing the Hermes bundle:
export TAG="$(jq -r .tag release-manifest.json)"
export VERSION="${TAG#v}"
export IMAGE=ghcr.io/maneek21/deft
export DIGEST="$(docker buildx imagetools inspect "$IMAGE:$VERSION" --format '{{json .Manifest.Digest}}' | tr -d '"')"
export SIGNATURE_IDENTITY="$(jq -r .signature_identity release-manifest.json)"
cosign verify "$IMAGE@$DIGEST" \
--certificate-identity "$SIGNATURE_IDENTITY" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
gh attestation verify "oci://$IMAGE@$DIGEST" --repo Maneek21/DeftVerify downloaded assets against SHA256SUMS; older checksum files may name
default.env.example as .env.example, so verify the unedited template under
that name before copying it to .env. Do not compare your secret-filled .env
against the template checksum. Image verification above additionally requires
jq, Cosign, Docker Buildx, and GitHub CLI.
Compare DIGEST with release-manifest.json and verify every downloaded asset
against SHA256SUMS. For a Hermes-certified release, also confirm the manifest's
Hermes archive and certificate digests before extracting the integration. That
scope requires two consecutive clean-state gates against the manifest-pinned
runtime and an archive that exactly matches the certified bundle. Core releases
omit those integration artifacts; they still require image signing, provenance,
SBOM and corresponding source. Then set DEFT_IMAGE to the immutable
ghcr.io/maneek21/deft@<digest> reference. Use init only for a fresh database.
Versioned release upgrades begin at v0.2.0-preview.1 and use the dedicated
upgrade service described below.
Preview.15 is a core release and carries no new Hermes certification or integration bundle; a configured employee identity is not proof of a running external runtime.
If you are working from a cloned repo with Node.js and pnpm available on the host, first complete the clone and configuration step below, then use the bootstrap wrapper:
pnpm install --frozen-lockfile
pnpm selfhost:bootstrapThat validates .env, builds and starts Compose, runs the database init, runs
the doctor, and runs the OAuth/MCP smoke test. For a demo/pilot environment
with seeded Testers Tomatoes data:
pnpm selfhost:bootstrap --seed-pilotFor a hardened production overlay:
pnpm selfhost:bootstrap --prodTo validate .env before starting or rebuilding containers:
pnpm selfhost:bootstrap --prod --check-onlyThe Docker-only path below remains supported and does not require host-side Node.js or pnpm.
git clone https://github.com/Maneek21/Deft.git
cd Deft
cp .env.example .envOpen .env and set these required values:
openssl rand -hex 32 # paste into POSTGRES_PASSWORD
openssl rand -hex 32 # paste into JWT_SECRET
openssl rand -hex 32 # paste into JWT_REFRESH_SECRET
openssl rand -hex 32 # paste into ENCRYPTION_KEYAll four are required for Docker, including local evaluation. ENCRYPTION_KEY
must be a unique, non-placeholder secret of at least 32 characters.
Leave OLLAMA_URL commented unless an Ollama server is actually running.
Otherwise Deft will correctly show AI features as off until a provider is
configured.
If you are deploying to a remote server, set the public URLs before the first build because Next.js bakes them into the browser bundle:
NEXT_PUBLIC_APP_URL=http://your-domain-or-ip:3000
NEXT_PUBLIC_API_URL=http://your-domain-or-ip:3001
NEXT_PUBLIC_WS_URL=http://your-domain-or-ip:3001
DEFT_SOURCE_CODE_URL=https://example.com/your-fork/tree/EXACT_COMMITOfficial images default this source offer to the exact upstream release. If
you expose a modified Deft server to users over a network, set
DEFT_SOURCE_CODE_URL to a no-charge, publicly reachable copy of the exact
Corresponding Source you run, including the scripts needed to build and install
it. The in-product /license page presents this link to users.
If signup or login shows "Failed to fetch", the browser is probably trying to
call the wrong API URL. Fix the NEXT_PUBLIC_* values. Rebuild source images;
for prebuilt releases, recreate the app container with up -d --force-recreate deft
and the same three Compose files so startup injects the new URLs.
docker compose build deft init doctor smoke
docker compose up -d postgresThis builds the app and one-shot tool images, then starts Postgres with pgvector.
The first build can take a few minutes. Building init, doctor,
and smoke alongside deft matters on updates because those services run
schema and verification code from the image.
Run the one-shot init service once:
docker compose run --rm initThe init service first refuses any database that already contains application
tables, then runs pnpm db:push-full && pnpm db:seed inside the Deft image.
No host Node.js or pnpm install is required for the Docker self-host path.
After successful initialization, start the app:
docker compose up -d deftdb:push-full enables the vector extension, syncs the schema, and applies the
supplemental SQL files for search indexes and safe metadata backfills that
Drizzle cannot fully express. db:seed seeds Defty, internal agent/tool bundles,
task templates, and first-party employee templates. It is idempotent and does not
insert demo users.
docker compose run --rm doctorThe doctor checks API health, web reachability, browser/API origin agreement, the Postgres schema, database/host clock agreement, and the platform seed.
Run the doctor on the API host so its clock check compares the systems that produce record timestamps. If database time differs by more than five seconds (allowing for request latency), synchronize the host and database/VM clocks before continuing. Creation and audit entries can use database time while updates and action runs use API time; clock drift can make an update appear earlier than creation. The doctor only checks time and never rewrites historical timestamps. Recheck after suspending or resuming a local VM.
Then run the public connector smoke test:
docker compose run --rm smokeThe smoke test verifies API health, OAuth discovery metadata, dynamic client
registration, MCP initialize, and the protected MCP auth challenge. To also
exercise an authenticated MCP tools/list call, set DEFT_MCP_BEARER_TOKEN in
.env to a personal MCP token from Settings -> MCP Access and rerun smoke.
Open http://localhost:3000, create the first account, and keep that account as
the owner/admin seat. Post a message in a space, then create and update a task.
Doctor and smoke check infrastructure and connector protocol behavior; they do
not prove first login, chat, task creation, or a model-driven approval flow.
Use selfhost:reset when the stack already exists and you want to return it to
a clean first-user experience. The command takes a Postgres backup by default,
stops only the app container, drops and recreates the application schema, clears
the local uploads volume, runs the supported init path, restarts
the app, and then runs doctor + smoke. It rebuilds the app and tool images before
the destructive reset so the validation containers match the checked-out code.
Empty internal workspace, ready for the first owner signup:
pnpm selfhost:reset --platform-only --forceFresh Testers Tomatoes/demo workspace:
pnpm selfhost:reset --seed-pilot --forceFor a public or production-overlay deployment, the command intentionally needs a second confirmation flag:
pnpm selfhost:reset --prod --platform-only --force --force-production-resetIf your server uses a site-specific Compose overlay, append it after the base and production files:
pnpm selfhost:reset --prod --compose-file compose.demo.yml --platform-only --force --force-production-resetPreview the exact plan without touching data:
pnpm selfhost:reset --prod --platform-only --dry-runTake only a backup:
pnpm selfhost:backup --prodReset safety notes:
--forceis required for any real reset.--force-production-resetis also required when URLs or overlays look public.- Backups are written to
./backupsunless--backup-diris supplied. - Use
--skip-buildonly when you intentionally want to reuse the existing Docker images. - The command never deletes Docker volumes or reverse-proxy state.
- Use
--keep-uploadsonly when you intentionally want files to survive.
For production, use the overlay that does not publish Postgres to host ports:
docker compose -f docker-compose.yml -f compose.prod.yml build deft init doctor smoke
docker compose -f docker-compose.yml -f compose.prod.yml up -d postgres
docker compose -f docker-compose.yml -f compose.prod.yml run --rm init
docker compose -f docker-compose.yml -f compose.prod.yml up -d deft
docker compose -f docker-compose.yml -f compose.prod.yml run --rm doctor
docker compose -f docker-compose.yml -f compose.prod.yml run --rm smokeIf default local ports are already occupied, change these values in .env
before building:
DEFT_WEB_PORT=3000
DEFT_API_PORT=3001
DEFT_BIND_HOST=127.0.0.1
DEFT_POSTGRES_PORT=5432The app still listens on ports 3000 and 3001 inside the container. These values only control host-side published ports.
For a full VPS, DNS, reverse proxy, and HTTPS runbook, see
docs/self-hosting-vps-domain-https.md.
After the first account is created, direct signup is blocked. Add teammates from Settings -> Members. Deft generates one-time invite URLs; share them out of band.
Core self-hosted account recovery does not send email. This is separate from the independently installed Email App. Password recovery is admin-generated: owners/admins create recovery URLs from Settings -> Members and share them manually.
Defty is seeded by docker compose run --rm init. It becomes active once a
usable AI provider is configured. Without a provider, Defty and AI features stay
off while chat, tasks, notes, calendar, wiki, and the dashboard continue to work.
Human employees can connect personal AI clients through Settings -> MCP Access. Create a personal token, choose read-only or write-enabled scopes, and paste the generated streamable HTTP MCP config into Claude Desktop, Claude Code, ChatGPT MCP clients, or any compatible MCP runtime. Personal tokens act as the user who created them. Writes create tasks, messages, and wiki pages under that user's identity. With the corresponding scopes, personal clients can also manage the user's notes, native calendar events, inbox state, assigned approvals, projects, saved task views, and bounded agent-employee state. Initial authentication, connector authorization, member administration, credentials, billing, and irreversible deletion remain UI-only. In that sense Deft is operationally headless after setup, not a UI-free product.
For a copyable CRM workflow, see the CRM AI assistant guide.
Deft exposes a streamable HTTP MCP endpoint at:
https://your-domain.com/api/mcp/v1
For remote AI apps that support OAuth MCP, use the public endpoint and Deft's OAuth discovery metadata:
https://your-domain.com/.well-known/oauth-protected-resource
https://your-domain.com/.well-known/oauth-authorization-server
For clients that support HTTP MCP but do not complete OAuth, create a personal token in Settings -> MCP Access and use it as a bearer token.
| Client | Status | Recommended setup |
|---|---|---|
| Claude Code | Verified with Deft remote HTTP MCP. Runtime tool use still depends on the user's Claude Code subscription/API access. | claude mcp add --transport http deft https://your-domain.com/api/mcp/v1, then authenticate if the client asks. |
| Codex CLI / IDE | Codex documents streamable HTTP MCP with bearer-token and OAuth support. | Add Deft as a streamable HTTP MCP server in Codex config. Use OAuth when available, or a personal bearer token from Settings -> MCP Access. |
| ChatGPT main app | Account gated. Developer Mode/custom MCP apps are not visible in every plan or workspace. | In an eligible ChatGPT workspace, create a custom app/connector that points at the Deft MCP endpoint and follows the OAuth flow. |
| Claude Desktop / Claude web | Support varies by surface and release channel. | Prefer OAuth when the client offers it; otherwise use a personal bearer token if HTTP MCP headers are supported. |
| Generic MCP runtime | Compatible when it supports streamable HTTP plus OAuth or bearer headers. | Point the runtime at /api/mcp/v1 and grant only the scopes needed for that workflow. |
Start pilots with read-only scopes (read:workspace, read:wiki,
read:tasks, read:messages, read:calendar, read:modules). Add write scopes only when the
user expects that AI client to create or update Deft records under their own
identity. The write scopes are write:tasks, write:messages, write:wiki,
write:calendar, write:workspace, and write:modules. App workflows additionally
use read:apps, invoke:apps, and read:app-runs. The broad workspace scope covers notes,
inbox state, approvals, projects, saved views, and owner/admin-only agent state;
it does not expose secrets or member administration.
Bring-your-own-agent employees connect through MCP:
POST https://your-domain.com/api/mcp/v1
Create an agent employee from Settings -> Agent Employees, copy the bearer token, and paste the generated MCP config into your runtime. Agent employee tokens act as that employee and are governed by the employee's trust level, approval rules, health status, and MCP audit log.
Personal tokens and agent employee tokens use the same endpoint, but they are not the same authority model. Use personal tokens when a human wants their own AI assistant to help with work. Use agent employee tokens when an autonomous or semi-autonomous runtime should show up as a shared coworker in Deft.
Personal ChatGPT and other MCP connections act as the authorizing user. Scopes
limit available tools; handlers also enforce the user's organization, role,
membership, and resource access. Personal native writes execute under that user
and do not automatically enter the Agent Employee approval queue. A personal
client with write:workspace can also exercise the user's permitted approval
actions, so grant that scope deliberately.
Defty and Agent Employees use agent identities. The API routes governed writes
through tool tiers and trust policy, then resolves required approvals on the
server. In the shared tier policy, Conservative auto-executes auto actions,
Standard also permits quick, and Autonomous also permits full except guarded
destructive/admin operations. Individual workflows can require stricter review.
Task creation is normally quick; posting a message is full. A task proposal
in the walkthrough is not a promise that every configured task write pauses.
The resolver checks review authority and action state and claims pending work before execution. Repeated approval of a completed action returns its existing state. Idempotency and stale-target checks vary by tool; do not assume global exactly-once execution or that every intervening edit invalidates a proposal. Inspect the proposed details and current target before approving.
Personal token/grant revocation blocks subsequent authenticated calls. It does not undo completed writes or withdraw content already read by an external client. Governed agent actions use signed receipts; personal MCP calls have OAuth/MCP audit history. Receipts use a deployment-held HMAC secret, not independent third-party proof. Receipt recording can fail after a write succeeds, so inspect the resulting record and operational logs when reconciling an uncertain result.
Knowledge capture stores durable wiki records and source citations. Visibility is scoped to the organization and, for restricted pages, the owner or space membership. A source link is evidence to inspect, not a guarantee that an AI summary is correct. Correct the durable knowledge record as well as the source when needed; do not assume editing a chat message rewrites every derived page or already-created task. Wiki editing and version history support human correction.
With a provider enabled, classification/extraction can send chat content to the configured model, and Defty can send prompts, retrieved messages, tasks, knowledge, and tool results needed for a request. Embedding and transcription features can send text or audio to their separately configured providers. Personal MCP clients and external employee runtimes receive the records returned by their permitted tools and have their own provider, retention, and cost policies. Self-hosting the workspace does not by itself keep model traffic local.
Without a Deft AI provider, normal chat, tasks, notes, manual knowledge editing, calendar, and auth remain usable. Model-driven extraction and Defty reasoning need a provider; deterministic capture paths may still run. A personal MCP client can supply its own AI independently of Deft's provider configuration.
| Variable | Required | Purpose | Default |
|---|---|---|---|
POSTGRES_PASSWORD |
Yes | Database password for Compose Postgres | none |
JWT_SECRET |
Yes | Signs access tokens | none |
JWT_REFRESH_SECRET |
Yes | Signs refresh tokens | none |
ENCRYPTION_KEY |
Yes for Docker/production | Encrypts stored credentials and signs receipts; at least 32 characters, unique and non-placeholder | none suitable for Docker |
DEFT_APPS_ENABLED |
No | Exact true enables the experimental Apps API; keep aligned with the web build flag |
false |
NEXT_PUBLIC_FEATURE_APPS |
No | Build-time public flag that exposes Apps in the web bundle; changing it requires rebuilding the image | false |
DEFT_APP_RUNS_ENABLED |
No | Exact true enables the App Run runtime and draining; invalid or missing keyrings then fail startup |
false |
DEFT_APP_RUN_KEYRINGS |
Only when App Runs enabled | Single-line versioned JSON document containing separate 32-byte base64 Run-encryption, receipt-signing, and fingerprint keyrings | none |
DEFT_APP_RUN_APP_ORIGIN_ENABLED |
No | Exact true admits reviewed connected-App actions to App Runs; requires both DEFT_APPS_ENABLED=true and the Run engine |
false |
DEFT_APP_AUTOMATIONS_ENABLED |
No | Exact true enables the bounded governed App-automation scanner and workers; requires Apps, App Runs, and App-origin Runs. Keep disabled until the installation has reviewed its connector, grant, budget, and backup/restore readiness |
false |
DEFT_APP_RUN_LEGACY_MCP_CUTOVER_ENABLED |
No | Exact true routes supported legacy MCP capability calls into App Runs; requires the Run engine to be enabled |
false |
DATABASE_URL |
No for Compose | External Postgres URL for non-Compose installs | derived |
NEXT_PUBLIC_APP_URL |
Recommended | Public web URL and invite-link base | http://localhost:3000 |
NEXT_PUBLIC_API_URL |
Recommended | Public API URL seen by browser | http://localhost:3001 |
NEXT_PUBLIC_WS_URL |
Recommended | Public WebSocket/API URL seen by browser | http://localhost:3001 |
DEFT_SOURCE_CODE_URL |
Required for modified public deployments | Public Corresponding Source URL shown on /license |
Image-embedded upstream source URL |
API_PORT |
No | API port inside the app container | 3001 |
DEFT_WEB_PORT |
No | Host port for web | 3000 |
DEFT_API_PORT |
No | Host port for API | 3001 |
DEFT_BIND_HOST |
No | Host address for local database publishing | 127.0.0.1 |
DEFT_POSTGRES_PORT |
No | Host Postgres port in local compose | 5432 |
ANTHROPIC_API_KEY |
No | Optional AI provider fallback | none |
OPENAI_API_KEY |
No | Optional AI provider/embedding/transcription fallback | none |
OPENROUTER_API_KEY |
No | Optional AI provider fallback | none |
OLLAMA_URL |
No | Optional local Ollama endpoint; set only when running | none |
R2_ENDPOINT / R2_ACCESS_KEY / R2_SECRET_KEY / R2_BUCKET |
No | Cloudflare R2 uploads | local uploads volume |
METRICS_SCRAPE_TOKEN |
No | Bearer token for /api/metrics and /health/queue; unset disables detailed telemetry |
none |
Governed App Runs stay off by default. Existing self-hosts do not need any Run variable and retain the legacy connector execution path. The engine, connected App intake, and legacy MCP intake are intentionally separate:
| Run engine | Connected App intake | Legacy MCP intake | Meaning |
|---|---|---|---|
| Off | Off | Off | Default legacy behavior; no Run keyrings required |
| On | Off | Off | Run service can decrypt, resume, and drain accepted work; no new governed intake |
| On | On | Off | Reviewed connected-App actions enter App Runs; legacy MCP remains legacy |
| On | Off | On | Exact default-off legacy MCP canary enters App Runs |
| On | On | On | Both independently reviewed intake planes enter the same governed engine |
| Off | On | Any | Invalid; API startup rejects the configuration |
| Off | Any | On | Invalid; API startup rejects the configuration |
Operators preparing the engine must generate three independent 32-byte keys with this Node.js command:
node -e "const c=require('node:crypto');const k=()=>c.randomBytes(32).toString('base64');console.log(JSON.stringify({schema_version:'deft.app_run_keyring.v1',run_encryption:{current:'enc-v1',keys:{'enc-v1':k()}},receipt_signing:{current:'sign-v1',keys:{'sign-v1':k()}},fingerprint:{current:'fp-v1',keys:{'fp-v1':k()}}}))"Store the single-line output verbatim as DEFT_APP_RUN_KEYRINGS, then set
DEFT_APP_RUNS_ENABLED=true while leaving
DEFT_APP_RUN_APP_ORIGIN_ENABLED=false and
DEFT_APP_RUN_LEGACY_MCP_CUTOVER_ENABLED=false. Add a new key ID and make it
current to rotate; keep older entries configured for reads and verification.
Back up this setting with the database. Removing a key that is still referenced
by retained Run payloads, receipts, or fingerprints is unrecoverable data loss.
Startup checks that inventory before any governed effect can execute.
Enable the separate legacy intake flag only for a certified canary. To stop new governed MCP admission without stranding accepted work, turn the intake flag off and leave the engine on until the Run ledger and queue are quiescent.
Enable DEFT_APP_RUN_APP_ORIGIN_ENABLED=true only after Apps are enabled and
the connected App has passed explicit grant review and activation. The flag
admits only exact persisted App bindings; it does not create a grant, connector,
or provider authority. To stop new App-origin admission without stranding
accepted work, turn this intake flag off and leave the Run engine and matching
keyring available until the governed ledger is quiescent.
Read Governed App Run operations before enabling the feature. It contains rotation, retirement, backup/restore, disaster recovery, split-control drain/pause behavior, and the exact rollback-floor rules.
Persistent data lives in Docker volumes:
| Volume | Contents |
|---|---|
pgdata |
PostgreSQL data |
uploads |
User-uploaded files |
Upgrades from a release that bundled Redis may leave an orphaned Redis
container and redisdata volume. Deft does not remove either automatically.
After confirming no other workload uses them, an operator may identify the
exact Compose project resources with docker ps -a and docker volume ls, then
remove those exact resources manually. Back up anything uncertain first.
For a source checkout, the complete recovery-set helper is:
pnpm selfhost:backupThe backup command briefly stops the app and writes a timestamped recovery
directory under ./backups. It contains the database, persistent uploads,
any legacy uploads found in the running container writable layer, .env
(including keyrings), Compose configuration, and the running image identity.
For the production overlay:
pnpm selfhost:backup --prodFor a recoverable release deployment, capture one stopped recovery point. This keeps the database and local uploads at the same point in time and preserves the configuration, App Run keyrings, and immutable image selection needed to decrypt and verify retained data. Run these commands from the downloaded release-asset directory. Choose and keep a stable Compose project name for the deployment:
export COMPOSE_PROJECT_NAME=deft
export RECOVERY="recovery-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$RECOVERY"
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml stop deft
container_id="$(docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml ps -aq deft)"
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml \
exec -T postgres pg_dump -U postgres --clean --if-exists --no-owner --no-privileges deft \
| gzip -9 > "$RECOVERY/database.sql.gz"
docker run --rm \
-v "${COMPOSE_PROJECT_NAME}_uploads:/source:ro" \
-v "$PWD/$RECOVERY:/backup" alpine:3.22 \
tar -C /source -czf /backup/uploads.tar.gz .
mkdir -p "$RECOVERY/legacy-container-uploads"
if [ -n "$container_id" ]; then
docker cp "$container_id:/app/apps/api/uploads/." "$RECOVERY/legacy-container-uploads/"
docker inspect "$container_id" --format '{{.Image}}' > "$RECOVERY/running-image-id.txt"
docker image inspect "$(cat "$RECOVERY/running-image-id.txt")" --format '{{json .RepoDigests}}' \
> "$RECOVERY/running-image-repo-digests.json"
fi
cp .env docker-compose.yml compose.prod.yml compose.release.yml release-manifest.json "$RECOVERY/"
cp SHA256SUMS "$RECOVERY/release-SHA256SUMS"
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml config --images \
> "$RECOVERY/configured-images.txt"
while IFS= read -r image; do
docker image inspect "$image" --format '{{json .RepoDigests}}'
done < "$RECOVERY/configured-images.txt" > "$RECOVERY/image-repo-digests.jsonl"
(cd "$RECOVERY" && find . -type f ! -name SHA256SUMS -print0 \
| sort -z | xargs -0 sha256sum > SHA256SUMS)
chmod 600 "$RECOVERY/.env" "$RECOVERY/database.sql.gz" "$RECOVERY/uploads.tar.gz"
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml start deftStore that directory together as a protected recovery artifact. .env contains
authentication secrets, provider credentials, and DEFT_APP_RUN_KEYRINGS when
configured. Losing a retained App Run key makes its encrypted payload or receipt
unrecoverable. If uploads use R2 or another external canonical store, back up and
restore that store with its provider tools instead of the local uploads volume;
the command above covers local uploads only. Keep the release manifest and
SHA256SUMS beside the recovery artifact when available.
Restore into a new empty directory and a new, empty Compose project first. Do not
run init and do not restore over an initialized application database:
mkdir deft-restore && cd deft-restore
cp /secure/recovery/.env /secure/recovery/docker-compose.yml \
/secure/recovery/compose.prod.yml /secure/recovery/compose.release.yml .
export COMPOSE_PROJECT_NAME=deft-restore
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml pull
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml up -d postgres
gunzip -c /secure/recovery/database.sql.gz \
| docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml \
exec -T postgres psql -v ON_ERROR_STOP=1 -U postgres deft
docker run --rm \
-v "${COMPOSE_PROJECT_NAME}_uploads:/target" \
-v /secure/recovery:/backup:ro alpine:3.22 \
tar -C /target -xzf /backup/uploads.tar.gz
docker run --rm \
-v "${COMPOSE_PROJECT_NAME}_uploads:/target" \
-v /secure/recovery/legacy-container-uploads:/legacy:ro alpine:3.22 \
cp -a /legacy/. /target/
# Set DEFT_IMAGE in .env to the exact prior repo digest recorded in
# running-image-repo-digests.json before pulling or starting the restored app.
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml up -d deft
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml run --rm doctor
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml run --rm smokeBefore switching traffic, compare the restored image list and recovery checksums,
sign in, open representative uploaded files, and exercise any configured provider
key. If App Runs are enabled, follow docs/app-run-operations.md from the matching
source archive to verify key inventory and receipts and to inspect pending or
unknown outcomes. Keep the original deployment stopped until these checks pass.
The recovery point loses writes accepted after the app was stopped; measure the
dump/archive/restore time during a rehearsal to set the installation's RPO/RTO.
To return to the original deployment after a rehearsal:
cd /path/to/original-release-directory
export COMPOSE_PROJECT_NAME=deft
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml start deftAvailable exports include wiki JSON/Markdown/CSV (GET /api/wiki/export, with
format=md or format=csv), note Markdown download, and calendar ICS feeds.
These are content exports, not a complete restorable workspace archive. Use
Postgres, uploads, and configuration backups for recovery.
The first supported versioned schema baseline is v0.2.0-preview.1. The
upgrader fingerprints an untracked database before adopting that baseline,
records checksums in deft_schema_migrations, applies each later migration in
a transaction, and refuses unknown, newer, or modified migration histories.
For a source checkout:
git pull --ff-only
pnpm selfhost:upgrade --prodFor a named GHCR release, first create the recovery artifact above using the
current .env and previous image digest, but leave deft stopped instead of
running the final start deft command. Then pin DEFT_IMAGE in the working
.env to the target digest. Keep Deft stopped and set RECOVERY to the absolute
recovery-folder path. Before replacing its container, copy any captured legacy
uploads into the persistent volume; older releases may have written files into
the container's writable layer.
set -euo pipefail
test -d "$RECOVERY" || { echo 'Set RECOVERY to the absolute recovery folder path.'; exit 1; }
(cd "$RECOVERY" && sha256sum -c SHA256SUMS)
if [ -d "$RECOVERY/legacy-container-uploads" ]; then
container_id="$(docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml ps -aq deft)"
uploads_volume="$(docker inspect "$container_id" --format '{{range .Mounts}}{{if eq .Destination "/app/uploads"}}{{.Name}}{{end}}{{end}}')"
test -n "$uploads_volume" || { echo 'Uploads volume not found. Keep Deft stopped and check its storage mounts.'; exit 1; }
docker run --rm -v "$uploads_volume:/target" -v "$RECOVERY/legacy-container-uploads:/legacy:ro" alpine:3.22 cp -a /legacy/. /target/
fiThen run using the target release's downloaded Compose assets:
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml pull deft upgrade doctor smoke
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml run --rm upgrade
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml up -d --force-recreate deft
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml run --rm doctor
docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml run --rm smokeThe source-checkout wrapper stops app writes, records the complete recovery set
and previous image identity before a mutable target tag can be pulled, overlays
any captured legacy container uploads into the corrected persistent volume,
then builds or pulls the target, runs the upgrade service, recreates the app,
and requires doctor plus MCP smoke to pass. Site-specific overlays can be
appended with --compose-file <file>.
Schema upgrades are forward-only. The migration ledger is checksummed and a failed migration transaction rolls back, but there is no automatic downgrade path after a successful upgrade. Recovery means stopping Deft, restoring the pre-upgrade Postgres backup and uploads backup, and running the exact previous image digest. Rehearse that restore on a disposable host before upgrading data that cannot be recreated.
For a source checkout, preview the exact sequence without changing data:
pnpm selfhost:upgrade --prod --release --dry-runDatabase-only checks are also available inside the target image:
pnpm db:upgrade --status
pnpm db:upgrade --dry-run
pnpm db:upgradeDo not run init, db:push-full, or raw db:migrate as an upgrade mechanism.
If the upgrader rejects a pre-preview or incomplete schema, restore the backup
and use a reviewed migration or a fresh deployment. Uploads remain in the
Docker volume and are not deleted by the upgrade command; back them up according
to your storage policy before high-risk changes.
See current limitations before upgrading production.
- Managed hosting or one-click cloud deployments
- Multi-org / multi-tenant mode
- Email delivery for invites or password resets
- Native Slack/Gmail/GitHub OAuth promises
- Managed agent runtime provisioning