Use when deploying a Stacks application - picking a deploy target (AWS, Hetzner, or a host you own over SSH), the deployment workflow (build → deploy), pre/post deploy hooks, server vs serverless mode selection, first-time deployment setup, rollback, deployment troubleshooting, or the buddy deploy command. For cloud infrastructure details (EC2, Lambda, CloudFormation, Route53, IAM, rpx, systemd), see stacks-cloud. Covers buddy deploy, config/cloud.ts, provider previews, release and rollback.
Install
npx skillscat add stacksjs/stacks/stacks-deploy Install via the SkillsCat registry.
Stacks Deployment
The deployment workflow for Stacks applications.
Deploy Targets
cloud.provider in config/cloud.ts decides where buddy deploy ships. CLOUD_PROVIDER in the
environment overrides it. With neither set, the provider is aws.
cloud.provider |
Target | What the deploy does |
|---|---|---|
'aws' (default) |
AWS | Generates infrastructure, creates or updates a CloudFormation stack |
'hetzner' |
Hetzner Cloud | Provisions the server through the Hetzner API, then deploys over SSH |
'ssh' |
A host you already own | Adopts and bootstraps the host over SSH, then deploys to it |
'fly' |
Fly.io Machines | Builds storage/framework/Dockerfile, pushes it to registry.fly.io, rolls the Machines under their leases |
Read the repo's own config/cloud.ts before assuming a target. This repo uses 'hetzner'.
Hetzner and ssh share one pipeline: a release tarball copied over SSH, systemd units per site,
and the rpx gateway in front. They differ in whether a server is created for you. AWS is a
different pipeline entirely.
Quick Deploy
buddy deployPreview the complete environment-aware plan before changing anything:
buddy deploy --dry-run
buddy deploy staging --dry-run
buddy deploy --dry-run --site docs
buddy deploy --dry-run --jsonThe preview uses the same environment transformation and ts-cloud site classification as a real
deploy. It reports the ordered validation, infrastructure, build, package, release, runtime,
gateway, DNS, TLS, and optional container operations. It exits before prerequisite setup, builds,
packaging, hooks, provider calls, persistence, DNS, TLS, or service restarts. The JSON form emits
a versioned STACKS_DEPLOY_PREVIEW_JSON= line for API and tool consumers.
Deployment Prerequisites
Automated production deployments on GitHub Actions and ReviewOS require a full provider
commit SHA and branch ref. Before prerequisite setup, Buddy verifies the checkout against
that SHA and accepts an unchanged origin head directly. If the head moved, Buddy fetches
the branch from origin (FETCH_HEAD only), completes shallow history and requires the
commit to be on it. Unavailable or still-shallow history stops deployment. Before shipping over SSH it
reads the release each site's current link serves and stops only when production
already runs a commit that landed after this one, so a deploy never goes backwards. A
verified commit newer than production deploys even when the head has moved on: requiring
the exact head starved production whenever pushes outpaced CI. A commit off the branch
(force-pushed away), a tag/PR ref, missing metadata, an unavailable remote or an
unreadable box stops the deployment. Ordinary manual deployments and staging deployments
keep their existing behavior; dry-run previews remain read-only.
AWS (provider: 'aws')
- AWS credentials configured:
buddy configure:aws - APP_KEY generated:
buddy key:generate(must be colon-separated format) - APP_URL set in
.env - Team configured in
config/team.ts
Hetzner (provider: 'hetzner')
HCLOUD_TOKENin the environment, orhetzner.apiTokeninconfig/cloud.ts~/.ssh/id_ed25519.pubon the machine deploying: the driver registers it on the server
SSH (provider: 'ssh')
- A host in
ssh.hosts, orTS_CLOUD_SSH_HOSTin the environment - A key that reaches it, and passwordless sudo when the user is not root
buddy server:doctorclean
Fly.io (provider: 'fly')
FLY_API_TOKEN(an org token lets the first deploy create the app)- Docker where the deploy runs, for the image build and push
fly.*inconfig/cloud.ts:regions,count,vm,volume,hostnames. Every.env.<env>
value becomes a Fly secret. A deploy never destroys Machines; extras are reported.
Preview deployments (Hetzner and ssh)
buddy deploy:preview pr-123 deploys the app as its own tenant (slug <slug>-pr-123, attached to
the app's box) at pr-123.<cloud.previews.domain | PREVIEW_DOMAIN>; deploy:preview:remove pr-123
removes its units, gateway route, cert units, files and DNS records by exact name. Secrets come
from .env.preview only (the deploy refuses without it), and a relative SQLite path gets its own
data dir. --base staging uses the staging box. New apps ship .github/workflows/preview.yml
(same-repo PRs only; needs DEPLOY_SSH_KEY, DOTENV_PRIVATE_KEY_PREVIEW, vars.PREVIEW_DOMAIN).
Deployment Flow
AWS
- Validation: checks APP_KEY format, AWS region, app URL, team config
- Build: compiles application for production
- Infrastructure: generates CloudFormation template via ts-cloud
- Failover replicas (only when a bucket sets
failover): creates each replica bucket in its
region before the stack, since the stack cannot and its replication rule needs it - Deploy: creates or updates the CloudFormation stack
- Capabilities: CAPABILITY_IAM, CAPABILITY_NAMED_IAM
- OnFailure: ROLLBACK
- Tags: Environment, Project, ManagedBy
- Failover access: lets the new distribution read each replica, then copies across objects
that predate replication
Hetzner and SSH
- Validation: same prerequisite checks, plus the ts-cloud persistent-state capability check
- Infrastructure: Hetzner creates or reuses the server; SSH checks and bootstraps the host
- Build and package: builds each site, packages a release tarball
- Release: copies over SSH, installs systemd units, restarts, preserves the prior release
- Gateway: writes
/etc/rpx/sites.d/<slug>.json - DNS and TLS: reconciles A records and issues certificates, when publishing is allowed
The LAN rule (provider: 'ssh' only)
An SSH host on a private address publishes no DNS, requests no Let's Encrypt certificate, skips
the CDN and skips mail reconciliation. It prints the LAN URLs instead.
Private means RFC1918, 100.64/10 CGNAT, loopback, link-local, IPv6 unique-local or link-local,
a .local / .internal / .lan / .intranet / .home.arpa name, or a bare single-label
hostname. Publishing an A record for 192.168.1.42 would point every visitor's browser at
whatever occupies that address on their own network, and an ACME challenge cannot reach a host
the internet cannot route to.
To publish: give the host a routable address, forward 80 and 443 to it, declare a domain on a
site, and set ssh.publicIp ('auto' discovers it at deploy time). TS_CLOUD_SSH_PUBLISH_DNS=1
forces publishing on for a host behind a port forward the deploy cannot see; 0 forces it off.
Deploy Hooks (cloud/deploy-script.ts)
export default {
beforeDeploy({ environment, region }) {
// Build assets, run tests, send notifications
console.log(`Deploying to ${environment} in ${region}`)
},
afterDeploy({ environment, region, outputs }) {
// Cache warming, smoke tests, database seeding
console.log('Public IP:', outputs.PublicIp)
console.log('DNS:', outputs.DNS)
console.log('Endpoint:', outputs.Endpoint)
}
}Deployment Modes
Modes apply to the AWS target.
Server Mode (default)
- EC2 instances with ALB
- Best for: full-stack apps, WebSocket support, persistent connections
- Config:
config/cloud.ts→mode: 'server'
Serverless Mode
- Lambda + API Gateway + CloudFront
- Best for: API-only, cost optimization, auto-scaling
- Config:
config/cloud.ts→mode: 'serverless'
First Deployment Checklist
# 1. Configure the provider
buddy configure:aws # aws
# hetzner: set HCLOUD_TOKEN
# ssh: buddy server:doctor, then buddy server:setup
# 2. Generate app key
buddy key:generate
# 3. Set environment variables
buddy env:set APP_URL https://myapp.com
buddy env:set APP_ENV production
# 4. Review cloud config
# Edit config/cloud.ts
# 5. Preview, then deploy
buddy deploy --dry-run
buddy deployCLI Commands
buddy deploy [env] # deploy; env is production | staging | development
buddy deploy --prod # same, by flag (--staging, --dev)
buddy deploy --dry-run # preview without changing anything
buddy deploy --site <name> # deploy one site to the existing server
buddy deploy --domain <d> # override the domain this deploy publishes
buddy deploy --yes # skip confirmation (required in CI)
buddy deploy:rollback [site] # activate a preserved release (--env, --to, --dry-run)
buddy server:doctor # preflight an ssh host
buddy server:setup # adopt and bootstrap an ssh host
buddy cloud --diff # preview infrastructure changes before deploying
buddy cloud --ssh # SSH into deployed server
buddy cloud:remove # tear down infrastructure (alias: undeploy)
buddy cloud:cleanup # clean retained resourcesGotchas
- The API reachability guard recognises
serve:api, sourceserve/api.js
entrypoints and compiledstorage/framework/runtime/production/api.js
bundles, including wrapper commands and custom site names. Page sites still
needAPI_URLorPORT_APIpointing to their API; a compiled API site does
not need a proxy back to itself. - Check
cloud.providerbefore advising anything provider-specific. Half the AWS advice is wrong
for a Hetzner or SSH box, and vice versa. buddy deploy --prodfrom a non-interactive shell refuses without--yes, rather than hanging
on the confirmation prompt.- APP_KEY must be colon-separated format (validated during deployment)
buddy cloud --diffshows changes BEFORE deploying, and only for AWS- Deploy hooks run in the deployment process on your machine, not on the target server
- On Hetzner and SSH, a project's
/etc/rpx/sites.d/<slug>.jsonis replaced wholesale, soproject.slugmust be unique on a shared box and the config must declare every domain that
fragment serves .env.<environment>is decrypted at deploy time and shipped as every site's.env, so declaretenantsinconfig/cloud.tson a shared box to strip other tenants' keys- A failover replica bucket (
infrastructure.storage.<name>.failover) lives outside the stack, sobuddy cloud:removeleaves it, and its data, behind on purpose - For infrastructure details, see the
stacks-cloudskill
Evidence and infrastructure contract
core/config/src/capabilities.ts marks the Hetzner and adopted SSH targets
experimental: their retained SSH-target tests do not prove live rollback/provider
conformance. AWS and Fly have runtime paths outside that registry's retained
deploy matrix; do not infer either supported or unsupported from their absence.
Read the actual deploy branch, preview and provider tests before claiming parity.
config/cloud.ts distinguishes the named tsCloud object (provider,
infrastructure, environments, sites) from the default Stacks wrapper. Follow the
installed file's shape rather than moving tsCloud settings into an unrelated
flat object. A cloud SQS queue definition does not implement QUEUE_DRIVER=sqs
in the Stacks queue package. Previews, destructive removal, secret writes and
provider mutations need the user's existing task authorization.