This guide walks you through deploying UHRP Storage Server on Google Cloud Platform (GCP) with continuous delivery via GitHub Actions. When you finish, you’ll have:
-
A single‑region Cloud Storage bucket that stores all UHRP data.
-
A Cloud Run service that handles uploads, billing, and API requests.
-
A Cloud Run service that handles the broadcasting and advertising of your UHRP data.
-
An HTTP Load Balancer that fronts both the bucket (static files) and Cloud Run (dynamic API) behind a custom HTTPS domain.
-
A GitHub Actions workflow (
deploy.yaml
) that rebuilds and redeploys automatically whenever you push to themaster
orproduction
branch.
Security note The IAM roles in this tutorial are intentionally permissive to minimise friction. Feel free to tighten them once everything works.
GCP Project with billing Create a new Project on Google Cloud Platform that you have owner or editor access and is funded
GitHub account & repo You will fork/clone and push the code to your own repository.
Domain name
Optional but recommended for the HTTPS front‑end (e.g. storage.example.com
).
gcloud CLI Only required for the few shell commands shown below. Everything else uses the Cloud Console UI.
-
Fork the original repository or clone and push:
git clone https://github.com/bitcoin-sv/storage-server.git cd storage-server git remote remove origin git remote add origin <your‑github‑repo‑url.git> git push origin master # or production for your mainnet branch
-
Verify that your repo contains
.github/workflows/deploy.yaml
. All future pushes tomaster
orproduction
will trigger this workflow.
-
Storage > Buckets > Create bucket.
-
Name
<unique‑bucket‑name>
(e.g.my‑uhrp‑bucket
). -
Region Select a single region (e.g.
us‑west1
). -
Storage class Choose Autoclass.
GCS will now shift each object between Standard and Nearline automatically, giving you the lowest possible storage cost when access patterns change. -
Data protection Turn the extras off:
-
Soft delete policy Unchecked → We don’t keep 7‑day recovery copies; deletes are immediate.
-
Object versioning Unchecked
-
Retention Unchecked
-
-
Access control Default; make sure Public access prevention is Off.
-
Create
-
Apply CORS rules: Run these commands from a terminal in the root of your cloned storage‑server repository (e.g., your local shell or Cloud Shell).
gcloud auth login gcloud config set project <id> gsutil cors set bucket-cors-config.json gs://<your-bucket-name>
bucket-cors-config.json
is provided in the repo root; edit it if you need to restrict origins.
Open APIs & Services > Library, search each API, click Enable. Open APIs & Services > Library, search each API, click Enable:
API | Why it’s needed |
---|---|
Cloud Run Admin API | Create, update, and route traffic to the Cloud Run service. |
Cloud Functions API | Deploys the notifier function used for on‑chain events. |
Cloud Logging API | Captures structured logs from Cloud Run and the load balancer. |
Cloud Pub/Sub API | Delivers bucket‑create events (and other messages) to the notifier. |
Eventarc API | Connects Cloud Storage events to Cloud Run/Functions reliably. |
Artifact Registry API | Hosts container images built by GitHub Actions. |
Cloud Build API | Builds the Docker image inside GCP when you deploy from source. |
Service Usage API | Lets other services (and the GitHub SA) enable additional APIs programmatically. |
Cloud Resource Manager API | Allows IAM policy updates at the project level. |
Identity and Access Management (IAM) API | Grants and tests roles for service accounts. |
Compute Engine API | Backs load balancer & VPC resources under the hood. |
It can take a minute or two for each API to flip to Enabled.
Run these two commands before assigning roles to service accounts. They give Eventarc and Pub/Sub the bucket/project access they need.
gcloud storage buckets add-iam-policy-binding gs://<your-bucket-name> \
--member=serviceAccount:service-$(gcloud projects describe <your-project-name> --format="value(projectNumber)")@gcp-sa-eventarc.iam.gserviceaccount.com \
--role=roles/storage.legacyBucketReader
gcloud projects add-iam-policy-binding <your-project-name> \
--member=serviceAccount:service-$(gcloud projects describe <your-project-name> --format="value(projectNumber)")@gs-project-accounts.iam.gserviceaccount.com \
--role=roles/pubsub.publisher
Replace <your-bucket-name>
/ <your-project-name>
with your bucket & project.
-
Create IAM & Admin > Service accounts > Create →
uhrp-github-deployer
. -
Grant roles:
-
Cloud Run Admin
-
Cloud Functions Admin
-
Cloud Build Editor
-
Artifact Registry Administrator
-
Storage Object Admin
-
Service Account User
-
-
Keys tab > Add key > JSON – download and keep safe. You’ll paste this into a GitHub secret.
-
Find it at
IAM & Admin > IAM
(email ends withcompute@developer.gserviceaccount.com
). -
Give it all of these roles:
-
Artifact Registry Administrator
-
Storage Bucket Viewer
-
Cloud Functions Admin
-
Cloud Run Admin
-
Eventarc Admin
-
Logs Writer
-
Service Account Admin
-
Service Account User
-
Storage Object Admin
-
This Service Account becomes the runtime identity for Cloud Run, so the bucket permissions propagate automatically.
-
Artifact Registry > Repositories > Create
-
Name uhrp
-
Format Docker
-
Region same region as your bucket (e.g., us‑west1)
Note the host part of the repository URL (e.g., us‑west1-docker.pkg.dev). Use this as your GCR_HOST / STAGING_GCR_HOST / PROD_GCR_HOST secret value.
The examples below use the
STAGING_
prefix. For a production deployment, create the same secrets withPROD_
instead and push to theproduction
branch.
Secret | What it's for | Example |
---|---|---|
STAGING_ADMIN_TOKEN | Bearer token granting admin API calls (e.g., content migration) | super‑secret‑admin‑token |
STAGING_BSV_NETWORK | Which Bitcoin SV network the server talks to | testnet or mainnet |
STAGING_GCP_BUCKET_NAME | The Cloud Storage bucket you created | my-uhrp-bucket |
STAGING_GOOGLE_PROJECT_ID | Your GCP project ID | my‑gcp‑project |
STAGING_GCR_HOST | Container registry host (gcr.io , us.gcr.io , or Artifact Registry host) |
us.gcr.io |
STAGING_GCR_IMAGE_NAME | Repository/image name for the container | uhrp/uhrp-storage |
STAGING_GCP_STORAGE_CREDS | Raw JSON for the runtime service account that Cloud Run uses to access the bucket | { "type": "service_account", ... } |
STAGING_GCR_PUSH_KEY | Base64‑encoded JSON of the deployer service‑account key (used by the Action to push the image) | ewogICJ0eXBlIjogInNlcnZpY2VfYWNjb3VudCIsIC4uLn0= |
STAGING_HOSTING_DOMAIN | Public HTTPS domain for your load balancer | storage.example.com |
STAGING_HTTP_PORT | Port your app listens on inside the container | 3104 |
STAGING_MIN_HOSTING_MINUTES | Minimum retention billed per upload | 15 |
STAGING_NODE_ENV | Node environment string passed to the app | staging or production |
STAGING_PRICE_PER_GB_MO | Monthly price (USD) per GB stored | 0.03 |
STAGING_SERVER_PRIVATE_KEY | 32‑byte hex private key used to sign on‑chain ops | 58d23bd395c55041c65d415357b524fc2f07802c58d23bd395c55041c65d4153 |
STAGING_WALLET_STORAGE_URL | URL of the Toolbox Wallet storage server | https://staging-storage.babbage.systems |
Add them in GitHub Settings > Secrets and variables > Actions.
deploy.yaml
will:
-
Authenticate to GCP using
STAGING_GCR_PUSH_KEY
andSTAGING_GOOGLE_PROJECT_ID
. -
Build & push the Docker image
<GCR_HOST>/<PROJECT>/<IMAGE_NAME>:branch‑sha
. -
Generate a
service.<branch>.yaml
manifest with all environment variables. -
Deploy/replace the Cloud Run service in the bucket’s region.
-
Deploy the notifier Cloud Function.
Every push to master
triggers the staging deployment. A push to production
does the same but reads the PROD_*
secrets.
-
Commit any change and push to
master
. -
Watch GitHub > Actions → Deployment run. Green check = success.
-
In GCP Cloud Run > Services, verify a new service appears in your chosen region. Click its URL to confirm the server responds.
-
The initial deploy is private, so you may see 403 Forbidden. Grant public invoke rights:
gcloud run services add-iam-policy-binding <SERVICE_NAME> \ --region <REGION> \ --platform managed \ --member="allUsers" \ --role="roles/run.invoker"
Replace <SERVICE_NAME>
with the Cloud Run service name and <REGION>
with your region (e.g., us‑west1
). After a minute, loading the service URL should return a JSON status instead of 403.
Setting | Value |
---|---|
Protocol | HTTPS |
IP version | IPv4 |
IP address | Create a new static IP (e.g., staging-uhrp-ingress-ip ) |
Port | 443 |
Certificate | Create new → Google‑managed (enter STAGING_HOSTING_DOMAIN , e.g., storage.example.com ) |
Redirect | Enable HTTP → HTTPS redirect |
Field | Value |
---|---|
Backend type | Serverless network endpoint group (SNEG) |
Serverless network endpoint groups | Create new |
Cloud Run region | Same region as your bucket |
Cloud Run service | Select the service deployed by GitHub Actions (the main storage service, not the notifier) |
Cloud CDN | Disabled |
Security policy | Default |
Save to create the backend service (e.g., uhrp-backend-service
).
Field | Value |
---|---|
Bucket | Your previously created bucket |
Cloud CDN | Enabled (leave defaults) |
Add a rule that routes CDN requests to the bucket and everything else to Cloud Run:
Host | Path | Backend |
---|---|---|
* |
/cdn/* |
Backend bucket |
Click Create and wait a few minutes. Then:
-
Create an A record pointing your hosting domain to the load‑balancer IP (This may take a few hours).
-
Wait for DNS propagation. The Google‑managed certificate will turn Active automatically.
-
Test: Go to https://uhrp-ui.bapp.dev/ and test uploading and downloading with your hosting domain.
Everything is now live, secure, and fronted by a global HTTPS load balancer.
-
Restrict Cloud Run to accept traffic only from the load balancer’s identity instead of
allUsers
. -
Replace broad roles with narrower ones (e.g., Storage Object Viewer instead of Admin). Be sure to update the bucket IAM accordingly.
-
Set up monitoring & alerts in Cloud Monitoring.
© 2025 – Feel free to adapt, improve, and PR!