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+).
One-command flow (recommended)
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:
- The scripts grant project-level IAM roles to your user, including
roles/secretmanager.admin. gcloud auth loginstores local credentials/tokens that can be reused if the machine is compromised.- Docker auth is configured for Artifact Registry and may persist in local Docker config.
- A local
GCR/.envfile contains project identifiers and deployment metadata.
Minimum cleanup if you ever used a shared/untrusted machine:
- Revoke IAM roles from your user account in the GCP project.
- Revoke local gcloud credentials and clear config.
- Remove Docker credential entries for Artifact Registry.
- Delete local
GCR/.envand 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/.envis 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:
- Checks for (or generates)
Htmx.ApiDemo/package-lock.json - Builds the Docker image from the repo root using
GCR/Dockerfile - Pushes the image to Artifact Registry
- Deploys to Cloud Run using
GCR/docker-compose.yml - Opens the service to public access (no authentication required)
- 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.json → ConnectionStrings.DefaultConnection |
Secret Manager (via --set-secrets) → deploy script |
MongoDbName |
appsettings.json → MongoDbName |
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_ENVIRONMENTisProductionbutappsettings.Production.jsonoverrides 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.