385 lines
12 KiB
Markdown
385 lines
12 KiB
Markdown
# 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
|
|
bash GCR/run-all.sh
|
|
# non-interactive (auto-run missing steps):
|
|
bash GCR/run-all.sh --yes
|
|
```
|
|
|
|
**Windows:**
|
|
```powershell
|
|
.\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):
|
|
```bash
|
|
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:
|
|
```bash
|
|
gcloud auth revoke --all
|
|
gcloud config configurations delete default --quiet || true
|
|
```
|
|
|
|
Credential cleanup (Windows PowerShell):
|
|
```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:
|
|
|
|
```bash
|
|
# Linux
|
|
cp GCR/.env.example GCR/.env
|
|
```
|
|
|
|
```powershell
|
|
# 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
|
|
bash GCR/scripts/00-install-gcloud.sh
|
|
```
|
|
|
|
**Windows** (PowerShell, run as Administrator):
|
|
```powershell
|
|
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
|
|
bash GCR/scripts/01-login.sh
|
|
```
|
|
|
|
**Windows:**
|
|
```powershell
|
|
.\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
|
|
bash GCR/scripts/02-setup-project.sh
|
|
```
|
|
|
|
**Windows:**
|
|
```powershell
|
|
.\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
|
|
bash GCR/scripts/03-create-secrets.sh
|
|
```
|
|
|
|
**Windows:**
|
|
```powershell
|
|
.\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
|
|
bash GCR/scripts/04-deploy.sh
|
|
# or with a custom image tag:
|
|
bash GCR/scripts/04-deploy.sh v1.0.0
|
|
```
|
|
|
|
**Windows:**
|
|
```powershell
|
|
.\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.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:**
|
|
```bash
|
|
source GCR/.env
|
|
gcloud run services update $SERVICE_NAME \
|
|
--region=$GCP_REGION \
|
|
--update-env-vars "MongoDbName=NewDatabaseName"
|
|
```
|
|
|
|
**Windows:**
|
|
```powershell
|
|
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:**
|
|
```bash
|
|
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:**
|
|
```powershell
|
|
# 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
|
|
bash GCR/scripts/03-create-secrets.sh
|
|
```
|
|
|
|
**Windows:**
|
|
```powershell
|
|
.\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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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`.
|