Deploy a remote instance on Google Cloud
This is the recommended GCP path for one public Kurabase instance today:
- build the reviewed production image with Cloud Build and store it in Artifact Registry;
- run the supplied
deploy/production/compose.yamlon one dedicated Compute Engine VM; - expose only the ordinary application gateway through HTTPS; and
- keep the administrative gateway on loopback, reached only through IAP/VPN or an equally private operator network.
This is deliberately not a kura start deployment. It uses a remote EVM RPC,
real owner/app/admin accounts, an explicit bootstrap, and no Anvil or local
Studio proxy. OP Sepolia (11155420) is a good first remote test chain.
Why this path, rather than Cloud Run
The current production topology has one stateful gateway process per signer and
a shared public instance.json record. Two replicas that share a signer can
race EVM transaction nonces, so the supplied deployment intentionally uses one
replica per role and a Recreate strategy. The current code is not a safe
Cloud Run deployment: a Cloud Run rollout or autoscaling policy can introduce
overlapping processes, and its ephemeral filesystem is not the Compose
instance volume.
Compute Engine executes the production Compose deployment without changing those guarantees. Move to the supplied Kubernetes manifest only after the single-instance path is stable; it still requires one replica per signer. It is not an HA claim.
1. Set the operator variables
On a trusted workstation with the Google Cloud CLI, choose a project and a region close to the RPC provider. These examples use Tokyo and a VM name that is unique within the project.
export PROJECT_ID=your-gcp-project-id
export REGION=asia-northeast1
export ZONE=asia-northeast1-b
export VM_NAME=kurabase-op-sepolia-1
export ADDRESS_NAME="$VM_NAME-ip"
export REPOSITORY=kurabase
export IMAGE_TAG="$(git rev-parse --short HEAD)"
export IMAGE="$REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/production:$IMAGE_TAG"
gcloud auth login
gcloud config set project "$PROJECT_ID"
gcloud services enable \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com \
compute.googleapis.com \
secretmanager.googleapis.com
# Reserve this before setting the public DNS record.
gcloud compute addresses create "$ADDRESS_NAME" --region="$REGION"
export PUBLIC_IP="$(gcloud compute addresses describe "$ADDRESS_NAME" \
--region="$REGION" --format='value(address)')"
The operator needs permission to create the shown resources. The workload VM uses a separate service account; do not put a downloaded service-account JSON key on it.
2. Build, push, and pin the production image
Create the Docker repository once, then build from the reviewed checkout. Cloud Build performs the Linux image build, avoiding an accidental ARM-only image from an Apple Silicon workstation.
gcloud artifacts repositories create "$REPOSITORY" \
--repository-format=docker \
--location="$REGION" \
--description='Kurabase production images'
gcloud builds submit \
--region="$REGION" \
--tag="$IMAGE" \
.
gcloud artifacts docker images describe "$IMAGE"
Record the resulting sha256 digest and use a digest reference in the VM
configuration, for example
asia-northeast1-docker.pkg.dev/PROJECT/kurabase/production@sha256:....
Never replace a reviewed deployment merely by reusing a mutable tag.
3. Bootstrap from a trusted machine, not the VM
Create the remote EVM instance exactly as in production mode
deployment. The operator machine receives the
bootstrap-only owner-private-key; the VM must not receive it.
Prepare /secure/kurabase/production.env and the five one-line files described
there, set KURA_CHAIN_ID=11155420 for OP Sepolia (or your chosen remote chain),
then run the one-shot bootstrap and export its public record:
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 run --rm --no-deps --entrypoint node gateway \
-e 'process.stdout.write(require("fs").readFileSync("/kura/instance.json"))' \
> instance.json
Validate and retain instance.json. It contains public chain metadata,
contract address, gateway addresses and the publishable key; it contains no
RPC URL, private key or server key. This separation is important: a compromise
of the gateway VM must not reveal the owner credential.
4. Put the runtime secrets in Secret Manager
The VM needs only these four values: the authenticated RPC URL, app gateway private key, admin gateway private key, and admin server key. Upload them from already-protected files; never pass their contents as a shell argument or add them to an environment file.
gcloud secrets create kura-rpc-url --replication-policy=automatic
gcloud secrets versions add kura-rpc-url \
--data-file=/secure/kurabase/secrets/rpc-url
gcloud secrets create kura-app-private-key --replication-policy=automatic
gcloud secrets versions add kura-app-private-key \
--data-file=/secure/kurabase/secrets/app-private-key
gcloud secrets create kura-admin-private-key --replication-policy=automatic
gcloud secrets versions add kura-admin-private-key \
--data-file=/secure/kurabase/secrets/admin-private-key
gcloud secrets create kura-admin-server-key --replication-policy=automatic
gcloud secrets versions add kura-admin-server-key \
--data-file=/secure/kurabase/secrets/admin-server-key
Create a dedicated VM identity and grant it access only to those exact secrets and to pull this image repository:
gcloud iam service-accounts create kurabase-runtime \
--display-name='Kurabase runtime'
export RUNTIME_SA="kurabase-runtime@$PROJECT_ID.iam.gserviceaccount.com"
for SECRET in kura-rpc-url kura-app-private-key kura-admin-private-key kura-admin-server-key; do
gcloud secrets add-iam-policy-binding "$SECRET" \
--member="serviceAccount:$RUNTIME_SA" \
--role='roles/secretmanager.secretAccessor'
done
gcloud artifacts repositories add-iam-policy-binding "$REPOSITORY" \
--location="$REGION" \
--member="serviceAccount:$RUNTIME_SA" \
--role='roles/artifactregistry.reader'
Pin a numeric Secret Manager version in the VM fetch script below. Do not use
the latest alias for a production deployment: secret rotation should be an
intentional version/configuration change followed by a gateway restart.
5. Create the VM and install the deployment files
Create a dedicated Ubuntu VM with the runtime identity and the
cloud-platform scope. Start with at least two vCPU and 8 GiB memory, then
size it based on replay time and gateway memory rather than guessing from local
Anvil usage.
gcloud compute instances create "$VM_NAME" \
--zone="$ZONE" \
--machine-type=e2-standard-2 \
--boot-disk-size=30GB \
--image-family=ubuntu-2404-lts-amd64 \
--image-project=ubuntu-os-cloud \
--service-account="$RUNTIME_SA" \
--scopes=cloud-platform \
--address="$PUBLIC_IP" \
--tags=kurabase-public
Install Docker Engine plus the Compose v2 plugin and the Google Cloud CLI on
the VM using the vendors' current Ubuntu instructions. Do this through IAP or
another restricted administrative route; do not open SSH to the world. The
runtime service account supplies access tokens automatically, so no gcloud auth login and no user credential file belongs on the VM.
Copy the reviewed Compose file, public instance metadata, and a non-secret
environment file to /opt/kurabase / /etc/kurabase:
gcloud compute scp --zone="$ZONE" deploy/production/compose.yaml \
"$VM_NAME:/tmp/compose.yaml"
gcloud compute scp --zone="$ZONE" instance.json \
"$VM_NAME:/tmp/instance.json"
On the VM, place them at /opt/kurabase/deploy/production/compose.yaml and
/opt/kurabase/instance.json. Create /etc/kurabase/production.env with
mode 0644; it contains only public configuration:
KURA_IMAGE=asia-northeast1-docker.pkg.dev/PROJECT/kurabase/production@sha256:PINNED_DIGEST
KURA_SECRETS_DIR=/run/kurabase/secrets
KURA_CHAIN_ID=11155420
KURA_OWNER_ADDRESS=0xOWNER
KURA_APP_GATEWAY_ADDRESS=0xAPP_GATEWAY
KURA_ADMIN_GATEWAY_ADDRESS=0xADMIN_GATEWAY
KURA_CONFIRMATIONS=2
KURA_APP_PORT=55421
KURA_ADMIN_PORT=55422
KURA_AA_ENABLED=true
KURA_PRIVY_APP_ID=your_public_privy_app_id
KURA_ZERODEV_PROJECT_ID=your_public_zerodev_project_id
KURA_PUBLIC_RPC_URL=https://public-rpc.example.invalid
The last four provider values are optional and public. KURA_PUBLIC_RPC_URL
must be an unauthenticated HTTPS endpoint; never copy the authenticated
kura-rpc-url secret into it. Do not add a Privy app secret, ZeroDev credential
or owner private key to this file.
Configure Docker's Artifact Registry credential helper for the VM's runtime identity, then hydrate the Compose volume with the public instance record:
sudo gcloud auth configure-docker asia-northeast1-docker.pkg.dev --quiet
sudo docker pull "$(grep '^KURA_IMAGE=' /etc/kurabase/production.env | cut -d= -f2-)"
sudo docker volume create kurabase-production_instance
sudo docker run --rm --user 1000:1000 \
--mount type=volume,src=kurabase-production_instance,dst=/kura \
--mount type=bind,src=/opt/kurabase/instance.json,dst=/tmp/instance.json,readonly \
"$(grep '^KURA_IMAGE=' /etc/kurabase/production.env | cut -d= -f2-)" \
node -e "require('fs').copyFileSync('/tmp/instance.json', '/kura/instance.json')"
kurabase-production_instance is the named volume generated by the fixed
Compose project name. Back up its instance.json alongside the image digest
and migration sources. Do not run docker compose down -v on this host.
6. Fetch role secrets on boot and start Compose
Use a root-owned boot script to write exact Secret Manager versions into the
host's /run tmpfs. Docker reads the files to mount them into the relevant
containers; the files are not persisted in the repository or on the VM disk.
The ordinary gateway never receives the admin key, and neither gateway receives
the owner key.
Create /usr/local/libexec/kurabase-fetch-secrets (root-owned, mode 0700):
#!/bin/sh
set -eu
install -d -m 0700 /run/kurabase/secrets
fetch() {
secret="$1"
file="$2"
# Change this explicit version during a planned rotation.
gcloud secrets versions access 1 --secret="$secret" > "/run/kurabase/secrets/$file.tmp"
chmod 0444 "/run/kurabase/secrets/$file.tmp"
mv "/run/kurabase/secrets/$file.tmp" "/run/kurabase/secrets/$file"
}
fetch kura-rpc-url rpc-url
fetch kura-app-private-key app-private-key
fetch kura-admin-private-key admin-private-key
fetch kura-admin-server-key admin-server-key
Then create /etc/systemd/system/kurabase.service:
[Unit]
Description=Kurabase production gateways
After=docker.service network-online.target
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/usr/local/libexec/kurabase-fetch-secrets
ExecStart=/usr/bin/docker compose --env-file /etc/kurabase/production.env -f /opt/kurabase/deploy/production/compose.yaml up -d --pull always gateway admin-gateway
ExecStop=/usr/bin/docker compose --env-file /etc/kurabase/production.env -f /opt/kurabase/deploy/production/compose.yaml stop gateway admin-gateway
[Install]
WantedBy=multi-user.target
Confirm Docker's location with command -v docker and correct the unit if it
is not /usr/bin/docker. Enable and inspect it:
sudo systemctl daemon-reload
sudo systemctl enable --now kurabase
sudo systemctl status kurabase
sudo docker compose --env-file /etc/kurabase/production.env \
-f /opt/kurabase/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
7. Give only the app gateway a public HTTPS route
Create a firewall rule that permits ports 80 and 443 only to VMs carrying the
kurabase-public tag. Point the application's DNS name, for example
api.example.com, at the VM's reserved external IP. Terminate TLS with Caddy,
Nginx, or a managed HTTPS load balancer and proxy only to
127.0.0.1:55421.
gcloud compute firewall-rules create kurabase-public-https \
--network=default \
--direction=INGRESS \
--action=ALLOW \
--rules=tcp:80,tcp:443 \
--source-ranges=0.0.0.0/0 \
--target-tags=kurabase-public
# Permit SSH only through Identity-Aware Proxy, not from the Internet.
gcloud compute firewall-rules create kurabase-iap-ssh \
--network=default \
--direction=INGRESS \
--action=ALLOW \
--rules=tcp:22 \
--source-ranges=35.235.240.0/20 \
--target-tags=kurabase-public
A minimal Caddy site is:
api.example.com {
reverse_proxy 127.0.0.1:55421
}
Do not add a route to port 55422: that is the administrative gateway.
For remote Dashboard/CLI administration, use an IAP SSH tunnel or a private
VPN/proxy, then authenticate with the owner/developer management flow or a
server key kept in a trusted backend. CORS does not need a custom application
proxy: the gateway itself supports the SDK's browser CORS requests.
8. Verify, monitor, and rotate
From a separate network, verify the public gateway with:
curl --fail https://api.example.com/health
curl --fail https://api.example.com/meta/v1/instance
Check that the result contains the expected chain, contract and public gateway address. Then test one RLS-permitted request and one expected RLS denial. Watch for native-gas balance, RPC failures/rate limits, event replay duration, readiness, memory use, transaction failures and private admin access.
For an image upgrade, build a new revision, replace only the digest in
production.env, and run sudo systemctl restart kurabase. Do not overlap
the same signer in two processes. For a gateway signer rotation, explicitly
delegate the replacement on the instance first, update its public address and
secret together, restart and verify it, and only then revoke the old grant.
GKE and the hosted-provider path
deploy/production/kubernetes.yaml is the next step when cluster policy,
private Services and Kubernetes NetworkPolicy are required. Create an Autopilot
cluster with the Secret Manager integration, materialize role-separated
secrets, configure the public kura-instance ConfigMap, replace the images
with the pinned Artifact Registry digest, and follow the Kubernetes section of
production mode deployment. Keep its one-replica
and Recreate settings.
The separate OP Sepolia/Privy service-provider prototype is not a generic Cloud Run deployment yet. Its provider runtime intentionally runs as a dedicated private Docker host because it creates per-instance gateway containers. A public multi-tenant provider still needs a managed Kubernetes/runner replacement, durable transactional control-plane storage, per-instance secret isolation, and a complete live Privy/ZeroDev acceptance run before it should be exposed for developer sign-up.