Production mode deployment
Production mode runs Kurabase gateways against an existing remote EVM RPC, with operator-managed keys and independently delegated gateway signers. It is separate from kura start: it starts no Anvil, uses no development mnemonic, and does not mount the local Studio operator proxy. The deployment assets are in deploy/production/.
This mode describes how to deploy the current experimental software. It does not add reorg handling, finality guarantees, high availability, privacy, or a contract audit. Evaluate on a remote test network first. Public-chain records and transactions are public; RLS controls gateway access and cannot make chain data private.
What runs where
| Component | Access | Purpose |
|---|---|---|
| One-shot bootstrap | Authenticated RPC and owner key | Deploy or attach the contract and explicitly delegate two signers |
| App gateway | RPC and app signer only | Ordinary sessions and RLS-governed data requests |
| Admin gateway | RPC, distinct admin signer and server key | Administrative SQL/migrations and owner/developer wallet management |
instance.json | Public metadata only | Chain, contract, replay start block, owner, gateway addresses, publishable key |
| Browser | HTTPS gateway URL, public metadata, wallet consent | User sessions or explicit remote wallet management |
The owner key is never mounted into either gateway. The app gateway receives neither the administrative signer nor the administrative server key. Each gateway requires an active grant on this specific contract. Revocation and administrator epochs still apply. The gateway executes SQL/RLS within that delegated trust boundary; the contract does not prove arbitrary SQL evaluation.
Prerequisites
- Docker with Compose v2, or a Kubernetes cluster with a NetworkPolicy-capable CNI.
- An HTTPS EVM RPC that supports transaction submission, code and receipt reads, and historical event queries from the contract's deployment block. Confirm the provider's log-range limits against your expected history; the current gateway does not provide a durable external indexer.
- Three distinct funded EVM accounts: owner, ordinary gateway, administrative gateway. All three need the chain's native gas asset for their respective transactions. Bootstrap never transfers funds automatically.
- The expected chain ID, the three public addresses, and a policy for receipt confirmations. Bootstrap defaults to two confirmations; chain-specific finality remains an operator decision.
- An HTTPS reverse proxy for application traffic and a private route for administration. The gateways currently allow cross-origin requests; configure allowed origins, request/rate limits, TLS and access controls at the proxy. Keep
/admin/*off the public app ingress. Do not expose the RPC provider credential to browsers.
Build from one reviewed source revision. Bootstrap and gateway startup verify the deployed runtime bytecode against the image's contract artifact. A differently compiled or upgraded contract needs an explicit compatibility review and matching runtime image; this workflow never upgrades the contract in place.
Prepare secrets and public configuration
Copy deploy/production/production.env.example to an operator-controlled file, for example /secure/kurabase/production.env. Set the expected chain and the three public addresses. The file contains no private keys or authenticated RPC URLs. KURA_SECRETS_DIR must be an absolute path outside the repository.
Use your secret manager to provision these single-line files:
File in KURA_SECRETS_DIR | Contents |
|---|---|
rpc-url | Full authenticated HTTPS provider URL |
owner-private-key | Owner's 0x-prefixed 32-byte key; bootstrap only |
app-private-key | Ordinary gateway's distinct private key |
admin-private-key | Administrative gateway's distinct private key |
admin-server-key | A cryptographically random server credential of at least 32 characters |
Do not use the public Anvil test accounts. Generate a server key with at least 32 random bytes in your secret manager. These are raw values, not shell KEY=value files. No script sources their contents as executable shell code.
Restrict the host secrets directory to its owner (0700). Docker Compose file secrets preserve host file permissions: ensure container UID 1000 can read each mounted file. One portable option is read-only 0444 files inside the restricted 0700 directory; a UID/GID-matched 0440 setup is preferable when available. Never make the enclosing secrets directory world-readable. Compose secrets are file mounts, not an encrypted vault; protect the host and its backups.
The deployment supports these optional public settings:
| Setting | Behavior |
|---|---|
KURA_CONTRACT and KURA_DEPLOYMENT_BLOCK | Attach an existing matching contract instead of deploying; both are required for attachment |
KURA_CONFIRMATIONS | At least one; defaults to two |
KURA_IMAGE | Image tag or immutable digest; defaults to kurabase/production:dev for source builds |
KURA_APP_PORT, KURA_ADMIN_PORT | Loopback host ports; defaults to 55421 and 55422 |
KURA_AA_ENABLED | true by default. Set false only for a development-compatible deployment with no AA write path. |
KURA_PRIVY_APP_ID, KURA_ZERODEV_PROJECT_ID | Browser-discoverable provider IDs. They are not secrets and are passed through the reviewed gateway allowlist. |
KURA_PUBLIC_RPC_URL | Browser-discoverable, unauthenticated HTTPS RPC URL. The launcher rejects credentials, queries and fragments. |
For attachment, the configured owner must be the contract's current owner. Use the actual deployment block, or zero to replay all history. Startup verifies the contract's creation event at any nonzero start block, preventing an accidentally later block from silently omitting history.
Docker Compose
Run from the repository root. These commands refer only to the production Compose file. For multiple instances on one Docker host, add a distinct -p INSTANCE_NAME to every command for each instance, use distinct host ports, and provision separate signer/server secrets and public settings. The project name isolates the state volume and network; a shared default project name would not.
docker compose --env-file /secure/kurabase/production.env \
-f deploy/production/compose.yaml build
docker compose --env-file /secure/kurabase/production.env \
-f deploy/production/compose.yaml --profile bootstrap run --rm bootstrap
docker compose --env-file /secure/kurabase/production.env \
-f deploy/production/compose.yaml up -d gateway admin-gateway
docker compose --env-file /secure/kurabase/production.env \
-f deploy/production/compose.yaml ps
curl --fail http://127.0.0.1:55421/health
curl --fail http://127.0.0.1:55421/meta/v1/instance
curl --fail http://127.0.0.1:55422/health
Bootstrap is an explicit one-shot profile; ordinary restarts never need the owner key. It writes only public metadata to the named instance volume. Export that metadata for backups and Kubernetes:
docker compose --env-file /secure/kurabase/production.env \
-f deploy/production/compose.yaml run --rm --no-deps --entrypoint node gateway \
-e 'process.stdout.write(require("fs").readFileSync("/kura/instance.json"))' \
> instance.json
Validate that the file is JSON and matches the expected chain and contract before distributing it. Store the owner key offline or revoke the deployment job's secret-manager access after activation. Deleting a container is not proof that an operator host has erased every prior copy of a secret.
Route the public HTTPS application origin to the app gateway's loopback port. Route administrative automation through a private network, VPN, or an authenticated administrative proxy. The supplied Compose services are loopback-bound and have no public listener. Use docker compose stop / start for lifecycle operations. Avoid down -v: it deletes the public deployment record and any recovery checkpoint, though it cannot delete on-chain data.
The production image runs as UID/GID 1000 with a read-only root filesystem, dropped capabilities, a bounded temporary filesystem and no-new-privileges. It contains the gateway, contract artifact and deployment scripts; it does not build or run Dashboard or the local Studio proxy.
Kubernetes
First complete the one-shot bootstrap above and export instance.json. This also works for a Kubernetes deployment: the owner key remains on the deployment operator rather than entering the cluster. Push the built image to your registry and replace both image references in deploy/production/kubernetes.yaml with the resulting immutable digest.
Create the namespace, public ConfigMap and isolated role Secrets from files. Replace the example absolute paths with your secret-manager materialization paths:
kubectl create namespace kurabase
kubectl -n kurabase create configmap kura-instance --from-file=instance.json
# Optional, public browser-discovery metadata. Do not put any credential here.
kubectl -n kurabase create configmap kura-gateway-public \
--from-literal=KURA_AA_ENABLED=true \
--from-literal=KURA_PRIVY_APP_ID=your_public_privy_app_id \
--from-literal=KURA_ZERODEV_PROJECT_ID=your_public_zerodev_project_id \
--from-literal=KURA_PUBLIC_RPC_URL=https://public-rpc.example.invalid
kubectl -n kurabase create secret generic kura-app \
--from-file=rpc-url=/secure/kurabase/secrets/rpc-url \
--from-file=private-key=/secure/kurabase/secrets/app-private-key
kubectl -n kurabase create secret generic kura-admin \
--from-file=rpc-url=/secure/kurabase/secrets/rpc-url \
--from-file=private-key=/secure/kurabase/secrets/admin-private-key \
--from-file=server-key=/secure/kurabase/secrets/admin-server-key
kubectl apply -f deploy/production/kubernetes.yaml
kubectl -n kurabase rollout status deployment/kura-gateway
kubectl -n kurabase rollout status deployment/kura-admin-gateway
Use a secret-manager CSI driver or External Secrets integration if available. Native Kubernetes Secrets need encryption at rest and appropriately restricted RBAC; base64 is not encryption. Do not commit Secret manifests. The supplied deployments do not mount a service-account token.
Both Services are cluster-private. Create an ingress appropriate to your controller, DNS and TLS certificate. Label only the controller namespace kurabase.io/app-ingress=true; the app NetworkPolicy allows that namespace to connect. It grants no public administrative ingress. Administrative client pods in the same namespace must carry kurabase.io/admin-client=true. Restrict who can create or label those pods. Verify your CNI enforces NetworkPolicy; these objects cannot provide isolation with a CNI that ignores them. Add environment-specific egress rules for DNS and your RPC provider.
Each role deliberately has one replica and a Recreate strategy. Multiple processes sharing a signer can race transaction nonces; the current executor also serializes writes only within one process. Do not increase replicas or introduce a rolling overlap for one signer. Separate delegated signers still require an explicit concurrency and nonce-management design before an HA rollout.
Readiness checks /health, which checks chain access. Startup allows up to 30 minutes for initial replay. Liveness checks the listening socket so a provider outage does not continuously restart the process. Adjust resource and startup budgets for your data volume, and alert on stalled replay and unhealthy readiness.
Server automation and browser access
Only trusted administrative services should read admin-server-key. The launcher passes it to the Rust gateway as KURA_DEV_SERVER_KEY because that is the current server-credential API name; in this mode it is operator-provisioned, never generated by the local stack. It also generates a distinct, undisclosed legacy KURA_SECRET_KEY at process start to satisfy the gateway's compatibility configuration. The app gateway uses fresh, undisclosed server credentials and never receives the admin service credential.
For SQL/migrations, a trusted backend sends the admin key as both apikey and Authorization: Bearer ... to the private administrative gateway. Keep it in the backend's secret manager. It is not a user session and must not be substituted for a wallet or identity-authority proof on user/RLS writes.
The browser receives only the gateway URL and publishable project key. Users sign ordinary session intents with their wallet or use independently authorized identity sessions. Remote Dashboard administration uses the distinct owner/developer AdminSession wallet flow against the administrative gateway through an operator-approved HTTPS route. Never expose the local Studio proxy or paste a server key into a production browser.
KURA_PUBLIC_RPC_URL is optional on the gateway launcher and intentionally absent from the supplied deployments. If a wallet needs chain-discovery metadata, configure an explicitly public, unauthenticated HTTPS RPC URL. The launcher rejects credentials, query strings and fragments; operators must also ensure the path itself contains no provider token. It never copies the private RPC URL into public metadata.
Privy and ZeroDev public discovery
Production enables the reviewed AA path by default: ZeroDev eip7702, with a
kernel-4337 fallback. If browser-side provider setup needs discovery IDs, set
only the public values below on the gateway workload:
KURA_AA_ENABLED=true
KURA_PRIVY_APP_ID=your_public_privy_app_id
KURA_ZERODEV_PROJECT_ID=your_public_zerodev_project_id
The production launcher allowlists those two identifier values and the fixed
AA modes. It does not inherit KURA_PRIVY_APP_SECRET, ZeroDev API/bundler or
paymaster credentials, or a session key into the gateway process. Supply
those through the Privy/ZeroDev integration's workload identity and secret
manager instead; never add them to an instance file, Docker environment, or
browser-delivered configuration. Set KURA_AA_ENABLED=false only for a
development-compatible deployment that intentionally has no user-owned AA
write path.
Hosted runner handoff
A control plane can use this same deployment boundary. Its internal runner request should identify an instance ID, a deployment version/image digest, expected chain ID and owner, app/admin gateway public addresses, and optionally an existing contract plus replay start block. References to RPC, deployment-owner, app signer, admin signer and admin server-key secrets belong in an authenticated internal request. They are secret-manager references, never raw values in a browser request, public database record, or log.
Use a unique state volume/directory and a serialized bootstrap job for each instance ID. Resolve secret references only inside the runner, mount the owner credential into the one-shot bootstrap alone, and mount role-specific secrets into the resulting gateways. An activation response may return public instance.json fields and the application/administrative URLs. Never return signer files, RPC credentials, owner keys or service credentials. Mark activation successful only after bootstrap verifies both contract grants and both gateways become healthy.
The supplied launcher and scripts are a process/deployment boundary, not an HTTP runner service. The control plane must authenticate runner calls, authorize instance ownership, enforce idempotency and store secret references with appropriate access controls. A gateway address delegated for another contract is not authorization for this instance.
Recovery, upgrades and rotation
Bootstrap checks the expected chain before any transaction. It records the predicted contract address and signed transaction hash before deployment broadcast. If it crashes, a subsequent run resumes that checkpoint instead of silently deploying a second database. A missing/dropped transaction is a deliberate error: inspect the recorded hash and owner nonce with private operator tooling, resolve the transaction, and only then repair the checkpoint. Never delete a checkpoint just to clear an error.
An interrupted process may leave bootstrap.lock. Confirm that no bootstrap is still running, inspect the checkpoint and chain, then remove only that instance's lock directory. Successful verification of an already initialized instance sends no transactions. A revoked or incorrectly privileged gateway fails verification; bootstrap does not revive an existing instance's revoked grants.
Back up instance.json, the exact source revision/image digest, contract artifact, migration sources, and operator secret-manager recovery information separately. The gateway reconstructs data from chain events on restart; replay is not a backup against chain reorgs, provider history loss, or contract bugs. Recovery requires the original contract and a provider retaining the required history.
To rotate a service credential, replace only the admin server-key secret and restart the administrative gateway. To rotate a gateway signer, fund a new account, explicitly delegate it on this instance with the correct privilege, update the corresponding public address and signer secret together, stop the old process, start and verify the replacement, then revoke the old grant. Existing sessions bind the old gateway address; clients need new sessions. Gateway replacement is an owner/developer operation, not automatic bootstrap reconciliation.
For image upgrades, preserve the public instance configuration and secrets, review protocol/bytecode compatibility, and restart one role at a time without overlapping a signer. Rollback uses the previous compatible image and the same chain state; it cannot undo already committed transactions. Owner transfers also require an explicit configuration and delegation review because grants inherit administrator epochs.
Monitor native token balances, RPC health and quotas, transaction failures/latency, schema revision progress, event replay duration, memory, readiness and private administrative access. Keep logs private: provider/client libraries may include sensitive request context in failures. Bootstrap deliberately emits bounded diagnostics without raw provider errors or signed transactions.
Verify the deployment implementation
node --test deploy/production/production.test.mjs
# Requires a local Anvil binary and compiled contract artifact; uses only test keys.
KURA_PRODUCTION_E2E=1 node --test deploy/production/production.test.mjs
# Requires the built production image and Docker; cleans up its test resources.
KURA_PRODUCTION_DOCKER_E2E=1 node --test deploy/production/docker-smoke.test.mjs
The integration check deploys to a temporary local chain, verifies separate grants and secret handoff, checks idempotent restart and existing-contract attachment, rejects a wrong chain, replay block or signer, and proves a revoked gateway is not revived. The Docker smoke checks the actual non-root image with isolated file secrets and a read-only root filesystem, then creates and queries real chain-backed data. These tests do not send transactions to your configured remote network.