Files
Htmx/GCR

Deploying to Google Cloud Run

This folder contains everything needed to deploy the Htmx app to Google Cloud Run — completely isolated from the application code.

Folder structure

GCR/
├── .env.example          ← copy to .env and fill in your values
├── Dockerfile            ← multi-stage AOT build (Linux/amd64)
├── entrypoint.sh         ← maps Cloud Run's PORT var to ASP.NET Core
├── docker-compose.yml    ← Cloud Run service definition (used by gcloud)
├── run-all.sh            ← smart Linux runner (checks + prompts)
├── run-all.ps1           ← smart Windows runner (checks + prompts)
└── scripts/
    ├── 00-install-gcloud.sh       / .ps1   ← install Google Cloud CLI
    ├── 01-login.sh                / .ps1   ← authenticate + configure Docker
    ├── 02-setup-project.sh        / .ps1   ← one-time GCP project setup
    ├── 03-create-secrets.sh       / .ps1   ← manage MongoDB secret
    └── 04-deploy.sh               / .ps1   ← build, push, and deploy

.sh scripts are for Linux. .ps1 scripts are for Windows (PowerShell 5.1+).

Instead of running each step manually, use the root runner. It checks each step, shows completed items, and prompts to run only missing steps.

Linux:

bash GCR/run-all.sh
# non-interactive (auto-run missing steps):
bash GCR/run-all.sh --yes

Windows:

.\GCR\run-all.ps1
# non-interactive (auto-run missing steps):
.\GCR\run-all.ps1 -Yes

Security on Untrusted Machines

Do not run these scripts on machines you don't control unless absolutely necessary.

Why this matters:

  1. The scripts grant project-level IAM roles to your user, including roles/secretmanager.admin.
  2. gcloud auth login stores local credentials/tokens that can be reused if the machine is compromised.
  3. Docker auth is configured for Artifact Registry and may persist in local Docker config.
  4. A local GCR/.env file contains project identifiers and deployment metadata.

Minimum cleanup if you ever used a shared/untrusted machine:

  1. Revoke IAM roles from your user account in the GCP project.
  2. Revoke local gcloud credentials and clear config.
  3. Remove Docker credential entries for Artifact Registry.
  4. Delete local GCR/.env and any temporary files.

Example role cleanup (Linux/macOS shell):

USER_EMAIL="your-user@company.com"
PROJECT_ID="your-project-id"

for ROLE in \
    roles/run.developer \
    roles/artifactregistry.writer \
    roles/iam.serviceAccountUser \
    roles/secretmanager.admin \
    roles/secretmanager.secretAccessor \
    roles/secretmanager.secretVersionAdder; do
    gcloud projects remove-iam-policy-binding "$PROJECT_ID" \
        --member="user:$USER_EMAIL" \
        --role="$ROLE" \
        --quiet
done

Credential cleanup:

gcloud auth revoke --all
gcloud config configurations delete default --quiet || true

Credential cleanup (Windows PowerShell):

gcloud auth revoke --all
gcloud config configurations delete default --quiet

Prefer a dedicated personal/admin workstation, or use a tightly scoped CI service account instead of broad user credentials.


Step 0 — Configure your .env file

Copy the example and fill it in:

# Linux
cp GCR/.env.example GCR/.env
# Windows
Copy-Item GCR\.env.example GCR\.env

Open GCR/.env in any editor and set:

Variable Description Example
GCP_PROJECT_ID Your GCP project ID my-htmx-project
GCP_REGION Cloud Run region us-central1
GCP_REPOSITORY Artifact Registry repo name htmx
SERVICE_NAME Cloud Run service name htmx-app
MONGODB_DATABASE_NAME Database name HtmxAppDb

Security note: GCR/.env is gitignored. Never commit it.

MongoDB note: The app connects to MongoDB at startup. Cloud Run containers do not have access to localhost:27017 — use MongoDB Atlas (cloud-hosted) or a MongoDB instance reachable over the internet/VPC.

Secrets note: The MongoDB connection string is not stored in .env. It's stored securely in Google Cloud Secret Manager. See Step 4 below.


Step 1 — Install the Google Cloud CLI

Run once on a new machine.

Linux:

bash GCR/scripts/00-install-gcloud.sh

Windows (PowerShell, run as Administrator):

Set-ExecutionPolicy RemoteSigned -Scope CurrentUser   # first time only
.\GCR\scripts\00-install-gcloud.ps1

After installation, open a new terminal and verify:

gcloud version

Step 2 — Log in

Authenticates your machine to GCP and configures Docker to push to Artifact Registry.

Linux:

bash GCR/scripts/01-login.sh

Windows:

.\GCR\scripts\01-login.ps1

A browser window opens for Google sign-in. Use the account that owns or has access to your GCP project.


Step 3 — Set up the GCP project (one time)

Enables APIs, creates the Artifact Registry repository, and grants your account the required IAM roles. If billing is not yet linked, the script prompts you to choose a billing account.

Linux:

bash GCR/scripts/02-setup-project.sh

Windows:

.\GCR\scripts\02-setup-project.ps1

This is safe to re-run — all operations are idempotent.

What it enables

API Purpose
run.googleapis.com Cloud Run service
artifactregistry.googleapis.com Docker image storage
secretmanager.googleapis.com Available for future use
cloudresourcemanager.googleapis.com IAM and project management

What IAM roles it grants (to your account)

Role Purpose
roles/run.developer Deploy and manage Cloud Run services
roles/artifactregistry.writer Push container images
roles/iam.serviceAccountUser Run the service under a service account
roles/secretmanager.admin Create/manage secrets and IAM policies (includes secretmanager.secrets.create)
roles/secretmanager.secretAccessor Read secret payloads (for validation/access workflows)
roles/secretmanager.secretVersionAdder Add/set new secret versions (rotate values safely)

Step 4 — Create secrets in Google Cloud Secret Manager

Store the MongoDB connection string securely:

Linux:

bash GCR/scripts/03-create-secrets.sh

Windows:

.\GCR\scripts\03-create-secrets.ps1

The script prompts for your MongoDB connection string, creates the secret in Secret Manager, and grants Cloud Run permission to access it. The secret is referenced by name (mongodb-connection-string) in the deploy script — never stored in .env.

This is a one-time setup. Re-run only if you need to update the MongoDB connection string.


Step 5 — Deploy

Builds the Docker image, pushes it to Artifact Registry, and deploys to Cloud Run.

If secrets are missing, the deploy script now performs a pre-check and prompts to run the secrets setup script before continuing.

Linux:

bash GCR/scripts/04-deploy.sh
# or with a custom image tag:
bash GCR/scripts/04-deploy.sh v1.0.0

Windows:

.\GCR\scripts\04-deploy.ps1
# or with a custom image tag:
.\GCR\scripts\04-deploy.ps1 -Tag v1.0.0

The script:

  1. Checks for (or generates) Htmx.ApiDemo/package-lock.json
  2. Builds the Docker image from the repo root using GCR/Dockerfile
  3. Pushes the image to Artifact Registry
  4. Deploys to Cloud Run using GCR/docker-compose.yml
  5. Opens the service to public access (no authentication required)
  6. Prints the live service URL

By default the image tag is the short git commit SHA (e.g. a3f4b7c). A timestamp is used if the directory is not a git repo.


How configuration reaches the app

The app reads configuration from environment variables. Cloud Run injects them at container startup — no config files needed in the image.

Environment variable Maps to Set by
ConnectionStrings__DefaultConnection appsettings.jsonConnectionStrings.DefaultConnection Secret Manager (via --set-secrets) → deploy script
MongoDbName appsettings.jsonMongoDbName GCR/.env → deploy script → docker-compose.yml
ASPNETCORE_ENVIRONMENT ASP.NET Core environment docker-compose.yml (hardcoded Production)
PORT Listening port Injected by Cloud Run (default 8080)

Secret Manager workflow:

  • Step 4 stores the MongoDB connection string in Cloud Run Secret Manager
  • Step 5 (deploy script) injects it via gcloud run services update --set-secrets=...
  • The container never sees the raw connection string; Cloud Run mounts it as an env var at runtime
  • Each time you update the secret, Cloud Run automatically uses the latest version

The GCR/entrypoint.sh script translates Cloud Run's PORT variable into ASPNETCORE_HTTP_PORTS at container startup, since ASP.NET Core does not read PORT directly.


Re-deploying after code changes

Just run Step 5 again. Each deployment gets a new image tag (git SHA), and Cloud Run creates a new immutable revision. Traffic is shifted to the new revision automatically.


Updating configuration

Non-sensitive config (MONGODB_DATABASE_NAME, etc.)

To change a non-sensitive value without rebuilding:

Linux:

source GCR/.env
gcloud run services update $SERVICE_NAME \
    --region=$GCP_REGION \
    --update-env-vars "MongoDbName=NewDatabaseName"

Windows:

gcloud run services update htmx-app `
    --region=us-central1 `
    --update-env-vars "MongoDbName=NewDatabaseName"

Sensitive config (MongoDB connection string)

To update the MongoDB connection string:

Linux:

source GCR/.env
# Read from stdin (paste the connection string and press Ctrl+D):
gcloud secrets versions add mongodb-connection-string \
    --data-file=- \
    --project=$GCP_PROJECT_ID

Windows:

# Use a temp file to avoid adding trailing newlines to the secret
$connectionString = Read-Host -AsSecureString "Enter MongoDB connection string"
$connectionStringPlainText = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($connectionString))
$TempFile = [System.IO.Path]::GetTempFileName()
try {
    [System.IO.File]::WriteAllText($TempFile, $connectionStringPlainText, [System.Text.Encoding]::UTF8)
    gcloud secrets versions add mongodb-connection-string --data-file=$TempFile --project=your-project-id
} finally {
    Remove-Item $TempFile -Force -ErrorAction SilentlyContinue
}

Or re-run the creation script:

Linux:

bash GCR/scripts/03-create-secrets.sh

Windows:

.\GCR\scripts\03-create-secrets.ps1

Cloud Run automatically uses the latest secret version on the next container start.


Troubleshooting

Docker build fails on npm ci

npm ci requires Htmx.ApiDemo/package-lock.json to exist. Generate it locally:

cd Htmx.ApiDemo && npm install

Then commit package-lock.json to the repository.

gcloud: command not found after install

Close and reopen your terminal. The installer adds gcloud to PATH, but the current shell session won't see it until restarted.

PERMISSION_DENIED errors during deploy

Run 02-setup-project again — it grants the required IAM roles. It is safe to re-run.

Cloud Run container crashes on startup

View logs in the GCP console (Cloud Run → your service → Logs tab), or:

gcloud run services logs read $SERVICE_NAME --region=$GCP_REGION --limit=50

The most common causes:

  • MongoDB connection string is wrong or unreachable from Cloud Run
  • ASPNETCORE_ENVIRONMENT is Production but appsettings.Production.json overrides something unexpectedly

Service URL returns 404 for all routes

The service is running but no route matched. Confirm the app started correctly by checking logs for Now listening on.