Skip to main content

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.yaml on 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.

Authoritative GCP references​