|
|
@@ -0,0 +1,487 @@ |
|
|
# Deploying TanStack Start Apps to Cloudflare Workers with GitHub Actions |
|
|
|
|
|
Maybe you're a vibe coder, or you're a seasoned vet who's frustrated with Next and server components. Either way, I'll show you how to set up continuous deployment for a TanStack Start application to Cloudflare Workers using GitHub Actions (securely, of course). |
|
|
|
|
|
**Assumption:** |
|
|
|
|
|
- You have a TanStack Start project initialized. |
|
|
- Package manager like `pnpm` or `npm` |
|
|
|
|
|
**Prereqs:** |
|
|
|
|
|
1. **Cloudflare Account:** Grab an API token with the appropriate template. |
|
|
|
|
|
2. **GitHub Repository:** Your TanStack Start project hosted on GitHub. |
|
|
|
|
|
3. **Node.js & pnpm:** Installed locally. |
|
|
|
|
|
4. **Wrangler CLI:** (Optional but recommended for local testing) Installed globally or as a dev dependency (`pnpm add -D wrangler`). |
|
|
|
|
|
5. **GitHub CLI (`gh`):** (Optional) Useful for managing secrets programmatically if you have a lot. |
|
|
|
|
|
## Step 1: Configure Build for Cloudflare Workers |
|
|
|
|
|
TanStack Start uses Vinxi, which builds upon Nitro. Ensure your build process is configured to output a Cloudflare Workers compatible format. |
|
|
|
|
|
Check your app configuration (in `app.config.ts`) and ensure the Nitro preset targets Cloudflare Workers `preset: 'cloudflare-module'`. |
|
|
|
|
|
How do we know this is an option? It's not in the Tanstack Start docs (currently), but you can just check the guts of the Vinxi deployment presets: |
|
|
|
|
|
```typescript |
|
|
/** |
|
|
* Not all the deployment presets are fully functional or tested. |
|
|
|
|
|
* @see https://github.com/TanStack/router/pull/2002 |
|
|
|
|
|
*/ |
|
|
|
|
|
declare const vinxiDeploymentPresets: readonly ["alwaysdata", "aws-amplify", "aws-lambda", "azure", "azure-functions", "base-worker", "bun", "cleavr", "cli", "cloudflare", "cloudflare-module", "cloudflare-pages", "cloudflare-pages-static", ...]; |
|
|
|
|
|
type DeploymentPreset = (typeof vinxiDeploymentPresets)[number] | (string & {}); |
|
|
``` |
|
|
|
|
|
Now, install the `unenv` package. |
|
|
|
|
|
`pnpm add unenv` |
|
|
|
|
|
The server property in `app.config.ts` should look like this: |
|
|
|
|
|
```typescript |
|
|
import { cloudflare } from "unenv"; |
|
|
|
|
|
... |
|
|
|
|
|
server: { |
|
|
preset: "cloudflare-module", |
|
|
unenv: cloudflare, |
|
|
}, |
|
|
``` |
|
|
|
|
|
- Verify that your build script (e.g., `pnpm build`) generates the worker entry point, typically at `.output/server/index.mjs`. |
|
|
|
|
|
## Step 2: Configure Cloudflare Worker (`wrangler.toml`) |
|
|
|
|
|
Create a `wrangler.toml` file in the root of your project. This file configures your Cloudflare Worker deployment. |
|
|
|
|
|
```toml |
|
|
|
|
|
# wrangler.toml |
|
|
|
|
|
# Replace with your desired worker name (must be unique within your account) |
|
|
|
|
|
name = "your-app-name" |
|
|
|
|
|
# Entry point for your worker, generated by the build process |
|
|
|
|
|
main = ".output/server/index.mjs" |
|
|
|
|
|
# Enable Node.js compatibility APIs if your app or dependencies need them |
|
|
|
|
|
compatibility_flags = ["nodejs_compat"] |
|
|
|
|
|
|
|
|
# Use a recent compatibility date |
|
|
|
|
|
compatibility_date = "2024-09-19" # Or a later date |
|
|
|
|
|
# Example: Serve static assets built by Vinxi/Vite |
|
|
|
|
|
# The binding name "ASSETS" must match how you reference it in your server code (if needed) |
|
|
|
|
|
# Check Vinxi/Nitro Cloudflare preset docs for asset handling specifics. |
|
|
|
|
|
# assets = { directory = "./.output/public/", binding = "ASSETS" } |
|
|
|
|
|
|
|
|
|
|
|
# Example: Route traffic from your custom domain to this worker |
|
|
|
|
|
# routes = [{ pattern = "your-domain.com", custom_domain = true }] |
|
|
|
|
|
|
|
|
|
|
|
# Define bindings for Cloudflare resources (KV, R2, D1, etc.) |
|
|
|
|
|
# These names must match the variable names used in your Worker code |
|
|
|
|
|
|
|
|
|
|
|
[[kv_namespaces]] |
|
|
|
|
|
binding = "YOUR_KV_BINDING_NAME" # e.g., SESSION_KV |
|
|
|
|
|
id = "your_kv_namespace_id" # Find in Cloudflare Dashboard |
|
|
|
|
|
|
|
|
|
|
|
[[r2_buckets]] |
|
|
|
|
|
binding = "PROFILE_BUCKET" # e.g., USER_PROFILES_BUCKET |
|
|
|
|
|
bucket_name = "user-profiles" # Your R2 bucket name |
|
|
|
|
|
# Add other bindings (D1 databases, Queues, etc.) as needed |
|
|
|
|
|
|
|
|
|
|
|
# [vars] section is NOT used for secrets during deployment with this GitHub Action setup. |
|
|
|
|
|
# Secrets are injected securely via the workflow. |
|
|
|
|
|
# Use a separate `.dev.vars` file for local development secrets. |
|
|
|
|
|
|
|
|
|
|
|
[observability] |
|
|
|
|
|
# Optional: Enable observability features if needed |
|
|
|
|
|
enabled = true |
|
|
|
|
|
``` |
|
|
|
|
|
- **`name`**: Your Worker's name on Cloudflare. |
|
|
|
|
|
- **`main`**: Path to the built server entry point. Adjust if your build output differs. |
|
|
|
|
|
- **`compatibility_flags`**: Crucial if your code uses Node.js APIs (`fs`, `path`, `crypto`, `http`, etc.). |
|
|
|
|
|
- **`compatibility_date`**: Use a recent date. |
|
|
|
|
|
- **`assets`**: (Optional) Configure if/how static assets are served directly by the Worker. Consult Vinxi/Nitro documentation for the best approach with the Cloudflare preset. |
|
|
|
|
|
- **`routes`**: (Optional) Define how traffic is routed to your worker (e.g., from a custom domain). |
|
|
|
|
|
- **Bindings (`[[kv_namespaces]]`, `[[r2_buckets]]`, etc.)**: Define connections to other Cloudflare resources. The `binding` name is how you access the resource in your Worker code (e.g., `env.PROFILE_BUCKET`). |
|
|
|
|
|
## Step 3: Set up GitHub Secrets |
|
|
|
|
|
**Never commit secrets directly into your code or `wrangler.toml`.** Use GitHub Actions secrets. |
|
|
|
|
|
1. Go to your GitHub repository > Settings > Secrets and variables > Actions. |
|
|
|
|
|
2. Click "New repository secret" for each secret your application needs at runtime _and_ for deployment. |
|
|
|
|
|
3. **Required Deployment Secrets:** |
|
|
|
|
|
- `CLOUDFLARE_API_TOKEN`: Generate a Cloudflare API token with "Edit Workers" permissions (My Profile > API Tokens > Create Token). |
|
|
|
|
|
- `CLOUDFLARE_ACCOUNT_ID`: Find this on the right sidebar of your Cloudflare dashboard's Workers & Pages overview page. |
|
|
|
|
|
4. **Application Secrets:** Add secrets for _all_ environment variables your deployed Worker needs (based on your `.env` file, excluding client-side `VITE_*` variables unless also needed server-side): |
|
|
|
|
|
- `DATABASE_URL` |
|
|
|
|
|
- _(Add any others specific to your application)_ |
|
|
|
|
|
## Step 4: Create GitHub Actions Workflow |
|
|
|
|
|
Create a file named `.github/workflows/deploy.yml`. This workflow automates the build and deployment process when you push to the `main` branch. |
|
|
|
|
|
```yaml |
|
|
|
|
|
name: Deploy to Cloudflare Workers |
|
|
|
|
|
|
|
|
on: |
|
|
|
|
|
push: |
|
|
|
|
|
branches: |
|
|
|
|
|
- main |
|
|
|
|
|
|
|
|
|
|
|
jobs: |
|
|
|
|
|
deploy: |
|
|
|
|
|
runs-on: ubuntu-latest |
|
|
|
|
|
name: Deploy |
|
|
|
|
|
permissions: |
|
|
|
|
|
contents: read |
|
|
|
|
|
steps: |
|
|
|
|
|
- name: Checkout |
|
|
|
|
|
uses: actions/checkout@v4 |
|
|
|
|
|
|
|
|
|
|
|
- name: Setup pnpm |
|
|
|
|
|
uses: pnpm/action-setup@v4 |
|
|
|
|
|
with: |
|
|
|
|
|
version: 10 |
|
|
|
|
|
|
|
|
|
|
|
- name: Setup Node.js |
|
|
|
|
|
uses: actions/setup-node@v4 |
|
|
|
|
|
with: |
|
|
|
|
|
node-version: 20 |
|
|
|
|
|
cache: "pnpm" |
|
|
|
|
|
|
|
|
|
|
|
- name: Install dependencies |
|
|
|
|
|
run: pnpm install --frozen-lockfile |
|
|
|
|
|
|
|
|
|
|
|
- name: Build application for Cloudflare Workers |
|
|
|
|
|
run: pnpm build |
|
|
|
|
|
env: |
|
|
|
|
|
VITE_BASE_URL: ${{ secrets.VITE_BASE_URL }} |
|
|
|
|
|
|
|
|
|
|
|
- name: Deploy to Cloudflare Workers |
|
|
|
|
|
uses: cloudflare/wrangler-action@v3 |
|
|
|
|
|
id: deploy |
|
|
|
|
|
with: |
|
|
|
|
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} |
|
|
|
|
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} |
|
|
|
|
|
secrets: | |
|
|
|
|
|
DATABASE_URL |
|
|
|
|
|
... rest of secrets |
|
|
|
|
|
env: |
|
|
|
|
|
DATABASE_URL: ${{ secrets.DATABASE_URL }} |
|
|
... rest of env vars |
|
|
|
|
|
``` |
|
|
|
|
|
**Wtf is this?:** |
|
|
|
|
|
- **`on: push: branches: [main]`**: Triggers the workflow on pushes to the `main` branch. |
|
|
|
|
|
- **`jobs: deploy: runs-on: ubuntu-latest`**: Defines a job named `deploy` running on the latest Ubuntu runner. |
|
|
|
|
|
- **`steps`**: The sequence of actions: |
|
|
|
|
|
- Checkout code. |
|
|
|
|
|
- Set up pnpm and Node.js (with caching). |
|
|
|
|
|
- Install dependencies using `pnpm install --frozen-lockfile`. |
|
|
|
|
|
- Build the application using `pnpm build`. (Note the optional `env` section for build-time variables). |
|
|
|
|
|
- Deploy using `cloudflare/wrangler-action@v3`: |
|
|
|
|
|
- `apiToken`, `accountId`: Provided from GitHub secrets. |
|
|
|
|
|
- `secrets:`: A multiline list of secret _names_ that the action should upload to Cloudflare Workers secrets. |
|
|
|
|
|
- `env:`: Makes the corresponding GitHub secrets available as environment variables _to the action itself_, which is necessary for the underlying `wrangler secret bulk` command. |
|
|
|
|
|
## Step 5: Local Dev |
|
|
|
|
|
For running `wrangler dev` locally: |
|
|
|
|
|
1. Create a `.dev.vars` file in your project root. |
|
|
|
|
|
2. Add your local environment variables/secrets to this file in `KEY=VALUE` format: |
|
|
|
|
|
```ini |
|
|
|
|
|
# .dev.vars (Add this file to .gitignore!) |
|
|
|
|
|
|
|
|
|
|
|
DATABASE_URL="your_local_or_dev_db_connection_string" |
|
|
|
|
|
# ... add all other secrets/variables needed locally ... |
|
|
|
|
|
``` |
|
|
|
|
|
3. **IMPORTANT:** Add `.dev.vars` to your `.gitignore` file. |
|
|
|
|
|
Wrangler will automatically load variables from `.dev.vars` when you run `wrangler dev`. |
|
|
|
|
|
## Step 6: Add to `.gitignore` |
|
|
|
|
|
Ensure your `.gitignore` file includes: |
|
|
|
|
|
``` |
|
|
|
|
|
# .gitignore |
|
|
|
|
|
|
|
|
|
|
|
# Secrets and Local Overrides |
|
|
|
|
|
.env |
|
|
|
|
|
.dev.vars |
|
|
|
|
|
|
|
|
|
|
|
# Build output |
|
|
|
|
|
.output/ |
|
|
|
|
|
dist/ |
|
|
|
|
|
.vinxi/ |
|
|
|
|
|
|
|
|
|
|
|
# Dependencies |
|
|
|
|
|
node_modules/ |
|
|
|
|
|
|
|
|
|
|
|
# Wrangler state |
|
|
|
|
|
.wrangler/ |
|
|
|
|
|
``` |
|
|
|
|
|
## Step 7: Send it |
|
|
|
|
|
Commit `wrangler.toml`, `.github/workflows/deploy.yml`, and your updated `.gitignore`. Push your changes to the `main` branch. |
|
|
|
|
|
```bash |
|
|
|
|
|
git add wrangler.toml .github/workflows/deploy.yml .gitignore CLOUDFLARE_DEPLOYMENT.md |
|
|
|
|
|
git commit -m "feat: Configure Cloudflare Workers deployment via GitHub Actions" |
|
|
|
|
|
git push origin main |
|
|
|
|
|
``` |
|
|
|
|
|
Check the "Actions" tab in your GitHub repo |
|
|
|
|
|
--- |
|
|
|
|
|
_(Optional: Script to Set GitHub Secrets)_ |
|
|
|
|
|
Setting secrets is a pain in the ass. Skip the manual work with the GitHub CLI (`gh`) to automate adding secrets from your local `.env` file to your GitHub repository. Run it from the `scripts` directory.\_ |
|
|
|
|
|
- _Create `scripts/set-github-secrets.sh` (ensure it's executable: `chmod +x scripts/set-github-secrets.sh`)_ |
|
|
|
|
|
```bash |
|
|
|
|
|
# scripts/set-github-secrets.sh |
|
|
|
|
|
#!/bin/bash |
|
|
|
|
|
|
|
|
|
|
|
# Reads secrets from ../.env and sets them in the current GitHub repo via gh cli. |
|
|
|
|
|
# Prerequisites: gh cli installed and authenticated (`gh auth login`). |
|
|
|
|
|
|
|
|
|
|
|
ENV_FILE="../.env" |
|
|
|
|
|
REPO_OWNER_AND_NAME="$(gh repo view --json nameWithOwner -q .nameWithOwner)" |
|
|
|
|
|
|
|
|
|
|
|
if [ -z "$REPO_OWNER_AND_NAME" ]; then |
|
|
|
|
|
echo "Error: Could not automatically determine repository name." |
|
|
|
|
|
exit 1 |
|
|
|
|
|
fi |
|
|
|
|
|
|
|
|
|
|
|
if [ ! -f "$ENV_FILE" ]; then |
|
|
|
|
|
echo "Error: .env file not found at $ENV_FILE" |
|
|
|
|
|
exit 1 |
|
|
|
|
|
fi |
|
|
|
|
|
|
|
|
|
|
|
echo "Setting secrets for repository: $REPO_OWNER_AND_NAME" |
|
|
|
|
|
|
|
|
|
|
|
while IFS= read -r line || [ -n "$line" ]; do |
|
|
|
|
|
line=$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') |
|
|
|
|
|
if [ -z "$line" ] || [[ "$line" =~ ^# ]]; then |
|
|
|
|
|
continue |
|
|
|
|
|
fi |
|
|
|
|
|
if [[ "$line" =~ ^VITE_ ]]; then |
|
|
|
|
|
echo "Skipping VITE_ variable: $line" |
|
|
|
|
|
continue |
|
|
|
|
|
fi |
|
|
|
|
|
if [[ "$line" =~ ^([^=]+)=(.*) ]]; then |
|
|
|
|
|
SECRET_NAME="${BASH_REMATCH[1]}" |
|
|
|
|
|
SECRET_VALUE="${BASH_REMATCH[2]}" |
|
|
|
|
|
SECRET_VALUE=$(echo "$SECRET_VALUE" | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//") |
|
|
|
|
|
echo "Setting secret: $SECRET_NAME ..." |
|
|
|
|
|
if gh secret set "$SECRET_NAME" --repo "$REPO_OWNER_AND_NAME" -b"$SECRET_VALUE"; then |
|
|
|
|
|
echo "Secret $SECRET_NAME set successfully." |
|
|
|
|
|
else |
|
|
|
|
|
echo "Error setting secret $SECRET_NAME." |
|
|
|
|
|
fi |
|
|
|
|
|
else |
|
|
|
|
|
echo "Skipping malformed line: $line" |
|
|
|
|
|
fi |
|
|
|
|
|
done < "$ENV_FILE" |
|
|
|
|
|
|
|
|
|
|
|
echo "Finished setting secrets." |
|
|
|
|
|
``` |
|
|
|
|
|
- _Run `./scripts/set-github-secrets.sh`_ |