Skip to content

Commit 16a39ee

Browse files
Kastier1claude
andcommitted
refactor(hosting-cli): rename gcp deploy to reflex cloud deploy --gcp, add docs
Flatten the `gcp` group into a single `deploy` command with a `--gcp` target flag so the surface can grow to other targets without nesting. `--gcp-project` becomes optional at the Click level and is validated in-function so the missing-target error fires first. Add a hosting doc (with Enterprise-only callout) covering prerequisites, options, what gets created in the GCP project, the env-allowlist security model, CI usage, and troubleshooting. Wire it into the sidebar's Self Hosting section and add `Gcp` -> `GCP` to the sidebar acronym map. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2b88415 commit 16a39ee

7 files changed

Lines changed: 205 additions & 32 deletions

File tree

docs/app/reflex_docs/pages/docs/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ def get_previews_from_frontmatter(filepath: str) -> dict[str, str]:
149149
"docs/events/special_events.md": "Special Events Docs",
150150
"docs/library/graphing/general/tooltip.md": "Graphing Tooltip",
151151
"docs/recipes/content/grid.md": "Grid Recipe",
152+
"docs/hosting/deploy-to-gcp.md": "Deploy to GCP",
152153
}
153154

154155

docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/item.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ def create_item(route: Route, children=None):
1313
# For "Overview", we want to keep the qualifier prefix ("Components overview")
1414
alt_name_for_next_prev = name if name.endswith("Overview") else ""
1515
# Capitalize acronyms
16-
acronyms = {"Api": "API", "Cli": "CLI", "Ide": "IDE", "Mcp": "MCP", "Ai": "AI"}
16+
acronyms = {
17+
"Api": "API",
18+
"Cli": "CLI",
19+
"Ide": "IDE",
20+
"Mcp": "MCP",
21+
"Ai": "AI",
22+
"Gcp": "GCP",
23+
}
1724
name = re.sub(
1825
r"\b(" + "|".join(acronyms.keys()) + r")\b",
1926
lambda m: acronyms[m.group(0)],

docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/learn.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ def get_sidebar_items_hosting():
250250
children=[
251251
hosting.self_hosting,
252252
hosting.databricks,
253+
hosting.deploy_to_gcp,
253254
],
254255
),
255256
]

docs/hosting/deploy-to-gcp.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
```python exec
2+
import reflex as rx
3+
```
4+
5+
# Deploy to GCP Cloud Run
6+
7+
The `reflex cloud deploy --gcp` command deploys a Reflex app to your own [Google Cloud Run](https://cloud.google.com/run) service. Reflex Cloud fetches a Cloud Run-ready Dockerfile and a `gcloud` deploy script, writes the Dockerfile into your project, and runs the script against the Google Cloud project you specify. The image is built on Cloud Build (so it works from any host OS, including Apple Silicon) and pushed to Artifact Registry.
8+
9+
```md alert info
10+
# Enterprise tier only.
11+
12+
Self-deploying to GCP Cloud Run is part of the **Enterprise tier** of Reflex Cloud. The control plane will return `403` to non-Enterprise tokens, and the CLI surfaces a clear error pointing at this. Contact [sales@reflex.dev](mailto:sales@reflex.dev) to upgrade.
13+
```
14+
15+
## Prerequisites
16+
17+
Before running the command, install and authenticate the local tools the deploy script invokes:
18+
19+
- `gcloud` — install from the [Google Cloud SDK docs](https://cloud.google.com/sdk/docs/install), then run:
20+
- `gcloud auth login`
21+
- `gcloud auth application-default login`
22+
- `docker` — required by `gcloud builds submit` for source upload.
23+
- `bash` — used to run the deploy script.
24+
25+
You also need:
26+
27+
- A GCP project with **billing enabled**. Without it, `gcloud services enable` fails with `UREQ_PROJECT_BILLING_NOT_FOUND`.
28+
- An Enterprise-tier Reflex Cloud subscription and a logged-in Reflex CLI (`reflex login`).
29+
30+
## Quick start
31+
32+
From the root of your Reflex app:
33+
34+
```bash
35+
reflex cloud deploy --gcp \
36+
--gcp-project my-gcp-project-id \
37+
--service-name my-reflex-app
38+
```
39+
40+
The CLI will:
41+
42+
1. Authenticate against Reflex Cloud and fetch the deploy manifest (Dockerfile + `gcloud` script).
43+
2. Print the manifest so you can review it.
44+
3. Write a `Dockerfile` into your project (after asking, if one already exists).
45+
4. Ask for confirmation, then run the `gcloud` script: enable the required APIs, create the Artifact Registry repository, build the image on Cloud Build, and deploy a public Cloud Run service.
46+
47+
When it's done, you'll get a service URL like `https://my-reflex-app-<project-number>.us-central1.run.app`.
48+
49+
## Options
50+
51+
| Option | Default | Description |
52+
| --- | --- | --- |
53+
| `--gcp` | _(required)_ | Selects the GCP Cloud Run target. |
54+
| `--gcp-project` | _(required)_ | The GCP **project ID** to deploy into. Project numbers are **not** accepted by `gcloud artifacts repositories`; use the project ID. |
55+
| `--region` | `us-central1` | Cloud Run region. |
56+
| `--service-name` | `reflex-app` | Cloud Run service name. |
57+
| `--ar-repo` | `reflex` | Artifact Registry repository name (created on first deploy). |
58+
| `--version` | UTC timestamp (`YYYYMMDD-HHMMSS`) | Image version tag. |
59+
| `--source` | `.` | Directory containing the Reflex app and into which the Dockerfile is written. |
60+
| `--overwrite-dockerfile` | _off_ | Overwrite an existing `Dockerfile` without prompting. |
61+
| `--token` | _from `~/.reflex` config_ | Reflex authentication token. |
62+
| `--interactive / --no-interactive` | `--interactive` | Whether to prompt before overwriting the Dockerfile and running the script. |
63+
| `--dry-run` | _off_ | Print the manifest without writing the Dockerfile or running the script. |
64+
| `--loglevel` | `info` | Log verbosity. |
65+
66+
## What gets created in your GCP project
67+
68+
The deploy script enables these APIs (if not already enabled):
69+
70+
- `cloudbuild.googleapis.com`
71+
- `run.googleapis.com`
72+
- `artifactregistry.googleapis.com`
73+
74+
It then creates (idempotently) and uses:
75+
76+
- An Artifact Registry Docker repository at `${REGION}-docker.pkg.dev/${GCP_PROJECT}/${AR_REPO}`.
77+
- A Cloud Build job that builds and pushes the image.
78+
- A Cloud Run service named `${SERVICE_NAME}`, deployed with `--allow-unauthenticated`, port 8080, 1 vCPU, 1 GiB memory, `--min-instances 1`, and `--session-affinity`.
79+
80+
Re-running the command pushes a new image tag and rolls the Cloud Run service forward.
81+
82+
## Security model
83+
84+
The CLI runs the deploy script under a **restricted environment**. Only an explicit allowlist of host variables is forwarded to `bash` — things like `PATH`, `HOME`, `CLOUDSDK_*`, `DOCKER_*`, and proxy/TLS variables. Unrelated host secrets such as `AWS_*`, `GITHUB_TOKEN`, or arbitrary user variables are **not** forwarded, so a tampered or compromised manifest cannot exfiltrate them.
85+
86+
You can preview the exact script and Dockerfile before anything runs by using `--dry-run`:
87+
88+
```bash
89+
reflex cloud deploy --gcp \
90+
--gcp-project my-gcp-project-id \
91+
--dry-run
92+
```
93+
94+
## Non-interactive use (CI)
95+
96+
For automated pipelines, pass `--no-interactive`, an explicit `--token`, and `--overwrite-dockerfile`:
97+
98+
```bash
99+
reflex cloud deploy --gcp \
100+
--gcp-project "$GCP_PROJECT_ID" \
101+
--service-name my-reflex-app \
102+
--token "$REFLEX_TOKEN" \
103+
--no-interactive \
104+
--overwrite-dockerfile
105+
```
106+
107+
In non-interactive mode the CLI will not prompt — it will refuse to overwrite an existing `Dockerfile` unless `--overwrite-dockerfile` is set, and it will exit non-zero if a token cannot be resolved.
108+
109+
## Troubleshooting
110+
111+
**`Flexgen denied the request (403). GCP Cloud Run deploys require an Enterprise tier subscription.`**
112+
Your account is not on the Enterprise tier. Contact [sales@reflex.dev](mailto:sales@reflex.dev).
113+
114+
**`Billing must be enabled for activation of service(s) ...` (`UREQ_PROJECT_BILLING_NOT_FOUND`)**
115+
Attach a billing account to the GCP project, or use a different `--gcp-project`.
116+
117+
**`The value of '--project' flag was set to Project number. To use this command, set it to PROJECT ID instead.`**
118+
Pass the project ID (e.g. `my-app-123456`), not the numeric project number.
119+
120+
**`No active GCP account found.`**
121+
Run `gcloud auth login` and `gcloud auth application-default login`.
122+
123+
**`The 'gcloud' / 'docker' / 'bash' CLI was not found on PATH.`**
124+
Install the missing tool and ensure it's on `PATH` for the shell you're invoking the CLI from.

packages/reflex-hosting-cli/src/reflex_cli/v2/deployments.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from reflex_cli import constants
1313
from reflex_cli.utils import console
1414
from reflex_cli.v2.apps import apps_cli
15-
from reflex_cli.v2.gcp import gcp_cli
15+
from reflex_cli.v2.gcp import deploy_command as gcp_deploy_command
1616
from reflex_cli.v2.project import project_cli
1717
from reflex_cli.v2.secrets import secrets_cli
1818
from reflex_cli.v2.vmtypes_regions import vm_types_regions_cli
@@ -66,8 +66,8 @@ def hosting_cli(ctx: click.Context) -> None:
6666
name="secrets",
6767
)
6868
hosting_cli.add_command(
69-
gcp_cli,
70-
name="gcp",
69+
gcp_deploy_command,
70+
name="deploy",
7171
)
7272
for name, command in vm_types_regions_cli.commands.items():
7373
# Add the command to the hosting CLI

packages/reflex-hosting-cli/src/reflex_cli/v2/gcp.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,19 @@
8181
})
8282

8383

84-
@click.group()
85-
def gcp_cli():
86-
"""Commands for deploying to GCP Cloud Run."""
87-
88-
89-
@gcp_cli.command(name="deploy")
84+
@click.command(name="deploy")
85+
@click.option(
86+
"--gcp",
87+
"use_gcp",
88+
is_flag=True,
89+
default=False,
90+
help="Deploy to GCP Cloud Run. Required (the only supported target today).",
91+
)
9092
@click.option(
9193
"--gcp-project",
9294
"gcp_project",
93-
required=True,
94-
help="The GCP project ID to deploy into (sets GCP_PROJECT).",
95+
default=None,
96+
help="The GCP project ID to deploy into (sets GCP_PROJECT). Required with --gcp.",
9597
)
9698
@click.option(
9799
"--region",
@@ -150,8 +152,9 @@ def gcp_cli():
150152
default=constants.LogLevel.INFO.value,
151153
help="The log level to use.",
152154
)
153-
def gcp_deploy(
154-
gcp_project: str,
155+
def deploy_command(
156+
use_gcp: bool,
157+
gcp_project: str | None,
155158
region: str,
156159
service_name: str,
157160
ar_repo: str,
@@ -163,15 +166,25 @@ def gcp_deploy(
163166
dry_run: bool,
164167
loglevel: str,
165168
):
166-
"""Deploy a Reflex app to GCP Cloud Run.
169+
"""Deploy a Reflex app to a cloud target.
167170
168-
Fetches a Dockerfile and bash deploy script from flexgen, writes the Dockerfile
169-
into the source directory, then asks before running the script.
171+
Currently the only supported target is GCP Cloud Run via --gcp. The command
172+
fetches a Dockerfile and bash deploy script from flexgen, writes the
173+
Dockerfile into the source directory, then asks before running the script.
170174
"""
171175
from reflex_cli.utils import hosting
172176

173177
console.set_log_level(loglevel)
174178

179+
if not use_gcp:
180+
console.error(
181+
"Specify a deploy target. Currently supported: --gcp (GCP Cloud Run)."
182+
)
183+
raise click.exceptions.Exit(2)
184+
if not gcp_project:
185+
console.error("--gcp-project is required when using --gcp.")
186+
raise click.exceptions.Exit(2)
187+
175188
authenticated_client = hosting.get_authenticated_client(
176189
token=token, interactive=interactive
177190
)

0 commit comments

Comments
 (0)