Deploy projects to hosts and cloud platforms.
The deploy command handles deployment to multiple platforms:
- Linux hosts via SSH (with systemd + nginx)
- Cloudflare Workers
- Railway
Auto-detects the platform from your flow.toml configuration.
If [flow].deploy_task is set, f deploy runs that task first.
If no deployment config exists but a deploy task is defined, f deploy runs that task.
Use f prod to deploy directly from [host], [cloudflare], [railway], or [web] (it skips [flow].deploy_task).
# Auto-deploy based on flow.toml config
f deploy
# Production deploy (skips flow.deploy_task, uses deploy config or deploy-prod task)
f prod
# Run the project's release task (flow.release_task or fallback)
f deploy release
# Deploy to specific platform
f deploy host
f deploy cloudflare
f deploy railway
# Production deploy to specific platform
f prod host
f prod cloudflare
# Configure deployment defaults
f deploy config
# Deploy web site
f deploy web| Command | Alias | Description |
|---|---|---|
host |
h |
Deploy to Linux host via SSH |
cloudflare |
cf |
Deploy to Cloudflare Workers |
web |
Deploy the web site (Cloudflare) | |
setup |
Interactive deploy setup (Cloudflare) | |
railway |
Deploy to Railway | |
config |
Configure deployment defaults (Linux host) | |
release |
Run the project's release task | |
status |
Show deployment status | |
logs |
View deployment logs | |
restart |
Restart the deployed service | |
stop |
Stop the deployed service | |
shell |
SSH into the host | |
set-host |
set |
Configure host for deployment |
show-host |
Show current host configuration | |
health |
Check if deployment is healthy |
Deploys the web site using Cloudflare and your project tasks. Flow will:
- Ensure
[web]exists inflow.toml(auto-fillspathwhen possible). - Add the
web.route(orweb.domain/*) to your wrangler config. - Optionally create/update the Cloudflare DNS record for the domain/route.
- Apply env vars from cloud if
web.env_source = "cloud"is set. - Run
deploy-web(ordeployas fallback).
f deploy webIf the Cloudflare API token is missing, Flow will guide you to create one and
store it in your env store as CLOUDFLARE_API_TOKEN.
If web.domain/web.route or web.path is missing, Flow will prompt for them
and update flow.toml.
If you opt into DNS management, Flow will prompt for record type/target and
create or update the Cloudflare DNS record (default: A to 192.0.2.1,
proxied).
If cloud is unavailable, Flow can store envs locally when you choose
web.env_source = "local".
Example flow.toml:
[web]
path = "packages/web"
domain = "example.com"
env_source = "cloud"
env_apply = "always"
env_keys = ["PUBLIC_API_URL"]
env_vars = ["PUBLIC_API_URL"]Deploy to any Linux server with SSH access. Flow handles:
- File syncing via rsync
- Systemd service creation
- Nginx reverse proxy setup
- SSL via Let's Encrypt
Add to flow.toml:
[flow]
deploy_task = "deploy-cli-release"
[host]
dest = "/opt/myapp" # Remote destination path
run = "./server" # Command to run the service
port = 3000 # Port the service listens on
service = "myapp" # Systemd service name (optional, defaults to folder name)
setup = "./scripts/setup.sh" # Setup script to run after first sync (optional)
env_file = ".env.production" # Path to .env file for secrets (optional)
env_source = "flow" # Pull envs from Flow env store (optional)
env_keys = ["API_KEY"] # Keys to fetch when env_source=flow/cloud (optional)
domain = "myapp.example.com" # Public domain for nginx (optional)
ssl = true # Enable SSL via Let's Encrypt (optional)Tip: f setup deploy can scaffold the [host] section and create a remote setup script.
First, configure your SSH connection:
# Set host (stored globally at ~/.config/flow/deploy.json)
f deploy set-host user@host:port
f deploy set-host deploy@myserver.com:22
f deploy set-host root@192.168.1.100
# Interactive config (prefills from ~/.config/infra/config.json if present)
f deploy config
# Verify connection
f deploy shell# Deploy to host
f deploy host
# Force re-run setup script
f deploy host --setup
# Build remotely instead of syncing local artifacts
f deploy host --remote-build- Sync files - rsync uploads project (excludes
target/,.git/,node_modules/,.env,*.log) - Copy env file - If
env_fileis specified, copies it to{dest}/.env(or, ifenv_source = "flow", fetches from Flow env store and writes{dest}/.env) - Run setup - Executes setup script on first deploy or with
--setup - Create systemd service - Generates and enables
/etc/systemd/system/{service}.service - Configure nginx - If
domainis set, creates reverse proxy config - Setup SSL - If
ssl = true, runs certbot for Let's Encrypt certificate - Start service - Runs
systemctl restart {service}
# View logs
f deploy logs # Since last deploy (host only)
f deploy logs -f # Follow in real-time (since last deploy)
f deploy logs --all # Full history (ignore deploy marker)
f deploy logs --all -n 500 # Show last 500 lines without deploy filter
# Restart/stop
f deploy restart
f deploy stop
# Check status
f deploy status
# SSH into server
f deploy shell
# Health check
f deploy health
f deploy health --url https://myapp.example.com/health
f deploy health --status 204 # Expect specific status codeNote: For host deploys, Flow records the last successful deploy time in .flow/deploy-log.json and uses it to scope f deploy logs output. Use --all to ignore it.
Deploy to Cloudflare's edge network.
Add to flow.toml:
[cloudflare]
path = "worker" # Path to worker directory (optional, defaults to project root)
environment = "production" # Wrangler environment name (optional)
env_file = ".env.cloudflare" # Path to .env file for secrets (optional)
env_source = "cloud" # Use cloud or local env store for secrets (optional)
env_keys = ["API_KEY", "SECRET"] # Specific keys to fetch from env store (optional)
env_vars = ["PUBLIC_URL"] # Keys to set as non-secret vars (optional)
deploy = "wrangler deploy" # Custom deploy command (optional)
dev = "wrangler dev" # Custom dev command (optional)
url = "https://my-worker.workers.dev" # URL for health checks (optional)- Wrangler CLI installed
wrangler.tomlin your worker directory- Authenticated with
wrangler login
# Deploy
f deploy cloudflare
# Deploy with secrets from env_file
f deploy cloudflare --secrets
# Run in dev mode
f deploy cloudflare --devUse [prod] to set a production domain or route for Workers. f prod will add
the route to your wrangler.json/jsonc before deploying.
[prod]
domain = "anysynth.nikiv.com" # Will be mapped to route "anysynth.nikiv.com/*"
# route = "anysynth.nikiv.com/*" # Use this for explicit patternsFor first-time setup, use the interactive wizard:
f deploy setupThis walks you through:
- Selecting worker directory (auto-discovers
wrangler.toml) - Choosing .env file for secrets
- Selecting Cloudflare environment (production, staging, etc.)
- Picking which secrets to push
- Updating
flow.tomlwith your choices
If your flow.toml lists service keys (for example Stripe keys), the setup flow
will offer to run the matching service onboarding before applying envs.
If using cloud for secret management:
[cloudflare]
env_source = "cloud"
env_keys = ["ANTHROPIC_API_KEY", "DATABASE_URL"] # Fetched as secrets
env_vars = ["PUBLIC_API_URL"] # Fetched as non-secret vars
environment = "production"Then deploy:
f deploy cloudflare --secretsIf you want to use local Flow envs instead:
[cloudflare]
env_source = "local"
env_keys = ["ANTHROPIC_API_KEY", "DATABASE_URL"]
env_vars = ["PUBLIC_API_URL"]If you need to fill missing values first:
f env guideDeploy to Railway's platform.
Add to flow.toml:
[railway]
project = "my-project" # Railway project ID
service = "api" # Service name (optional)
environment = "production" # Environment name (optional)
start = "npm start" # Start command (optional)
env_file = ".env.railway" # Path to .env file (optional)- Railway CLI installed (
npm install -g @railway/cli) - Authenticated with
railway login
f deploy railwayWhat happens:
- Links to Railway project if specified
- Sets environment variables from
env_file - Deploys with
railway up --detach
Check if your deployment is responding:
# Use domain from [host] or url from [cloudflare] config
f deploy health
# Custom URL
f deploy health --url https://api.example.com/health
# Expect specific status code
f deploy health --status 204Returns:
Healthy (HTTP 200 in 0.15s)on successUnhealthy: expected HTTP 200, got 500on wrong statusUnreachable: Connection refusedon network error
Host connection is stored globally at ~/.config/flow/deploy.json:
{
"host": {
"user": "deploy",
"host": "myserver.com",
"port": 22
}
}View/set:
f deploy show-host
f deploy set-host deploy@newserver.com:2222# flow.toml
[host]
dest = "/opt/api"
run = "/opt/api/server"
port = 8080
service = "myapi"
setup = "cargo build --release && cp target/release/server /opt/api/"
env_file = ".env.production"
domain = "api.mycompany.com"
ssl = true# First time setup
f deploy set-host root@myserver.com
# Deploy
f deploy host
# Check it's working
f deploy health
f deploy logs -f# flow.toml
[cloudflare]
path = "packages/worker"
environment = "production"
env_source = "cloud"
env_keys = ["OPENAI_API_KEY", "WEBHOOK_SECRET"]
url = "https://my-worker.mycompany.workers.dev"# Store project secrets
f env project set -e production OPENAI_API_KEY=sk-...
f env project set -e production WEBHOOK_SECRET=whsec_...
# Deploy with secrets
f deploy cloudflare --secrets
# Verify
f deploy health# GitHub Actions
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
run: |
f deploy set-host ${{ secrets.DEPLOY_HOST }}
f deploy host
f deploy healthRun f deploy set-host user@host:port first.
Ensure wrangler.toml exists in your worker directory, or run wrangler init.
Test with f deploy shell to debug. Check:
- SSH key is in
~/.ssh/and added to server - Port is correct (default: 22)
- Server is reachable
For Cloudflare, use --secrets flag: f deploy cloudflare --secrets
Check URL is correct and service is running:
f deploy logs -f # Check for errors
curl -v https://your-domain.com # Test manually