Deployment
On this page
- The deploy command
- Deploying to every target at once
- local
- github-pages
- s3
- azure
- gcs
- firebase
- ftp / sftp
- rsync
- netlify
- vercel
- cloudflare-pages
- The package command
- GitHub Actions (multi-version publishing)
- What it does
- One-time setup
- Publishing two versions at once
- Using this for your own project
- Serving from a project Pages sub-path
- Restricting who can reach your site
Deployment
site/ is a plain static site - host it anywhere that serves static
files. bxSites deploy ships it there
directly, in one command: S3 (and any S3-compatible service - DigitalOcean
Spaces, Cloudflare R2, Backblaze B2, MinIO), Azure Blob Storage, Google
Cloud Storage, Firebase Hosting, FTP, SFTP, rsync-over-SSH, Netlify,
Vercel, Cloudflare Pages, a local directory, or GitHub Pages.
The deploy command
Every target but the two simplest (local/github-pages, which work from
flags alone - see CLI Reference) is
configured via a deployments/<name>.json file at your project root - one
file per deploy target you actually use, each naming which target it is
plus that target's own fields:
bxSites deploy --entry=production
{ "target": "s3", "bucket": "my-docs-site", "accessKeyIdEnvVar": "AWS_ACCESS_KEY_ID", "secretAccessKeyEnvVar": "AWS_SECRET_ACCESS_KEY" }
Secrets always come from an environment variable, never from a literal
value in deployments/*.json. Every field ending in EnvVar names the
environment variable holding the real secret (an access key, a password,
an API token) - resolved live at deploy time, so deployments/*.json
itself is always safe to commit. A field that's a path to a credential
file you already manage yourself (an SSH private key, a downloaded GCP
service-account JSON key) is the one exception - a plain field, since the
file itself is what's kept out of version control, not its path. Locally,
those environment variables can come from a .env file too (BoxLang loads
one automatically and getSystemSetting() - what every target uses to
resolve them - checks it transparently) instead of exporting them into
your shell by hand; in CI, set them as real secrets on the runner.
Deploying to every target at once
Run bxSites deploy with neither --entry nor --target and every
deployments/*.json entry is deployed in turn, off a single shared build:
bxSites deploy
The site is only built once no matter how many entries you have. One
target failing doesn't stop the rest - every entry is attempted, and the
command only exits non-zero if at least one of them failed; the summary
reports how many succeeded (e.g. Deployed to 2/3 target(s) (1 failed)).
Add --verbose (works with --entry/--target too) to print a progress
line as the build and each target start and finish, instead of just the
final summary.
local
Copies the built site to any directory - a shared drive, a staging folder,
anywhere. The only target that needs no deployments/ entry at all.
bxSites deploy --target=local --destination=/path/to/somewhere
github-pages
The same push gh-deploy does, reachable
from this unified command too - also needs no deployments/ entry:
bxSites deploy --target=github-pages [--branch=gh-pages] [--remote=origin] [--message="..."]
s3
Real AWS S3, or any S3-compatible service - set endpoint for anything
other than AWS itself, and forcePathStyle: true for most non-AWS
providers.
{
"target": "s3",
"bucket": "my-docs-site",
"region": "us-east-1",
"prefix": "",
"accessKeyIdEnvVar": "AWS_ACCESS_KEY_ID",
"secretAccessKeyEnvVar": "AWS_SECRET_ACCESS_KEY"
}
{
"target": "s3",
"bucket": "my-docs-site",
"endpoint": "https://nyc3.digitaloceanspaces.com",
"forcePathStyle": true,
"accessKeyIdEnvVar": "SPACES_KEY",
"secretAccessKeyEnvVar": "SPACES_SECRET"
}
The same shape (custom endpoint + forcePathStyle: true) also covers
Cloudflare R2 (https://<accountid>.r2.cloudflarestorage.com), Backblaze
B2, and MinIO/Wasabi.
azure
Azure Blob Storage, authenticated with a SAS token, an account key, or a full connection string - exactly one of the three.
{
"target": "azure",
"account": "mystorageaccount",
"container": "site",
"accountKeyEnvVar": "AZURE_STORAGE_KEY"
}
gcs
Google Cloud Storage, authenticated with a downloaded service-account JSON key (Google Cloud Console -> IAM & Admin -> Service Accounts -> Keys).
{
"target": "gcs",
"bucket": "my-docs-site",
"serviceAccountKeyPath": "/path/to/service-account.json"
}
firebase
Firebase Hosting, using the same kind of service-account key as gcs.
{
"target": "firebase",
"siteId": "my-firebase-site",
"serviceAccountKeyPath": "/path/to/service-account.json"
}
ftp / sftp
Uploads the whole site to a remote server over FTP or SFTP, preserving its folder structure. SFTP accepts a password or an SSH key.
{
"target": "sftp",
"host": "example.com",
"username": "deploy",
"remotePath": "/var/www/html",
"key": "/home/me/.ssh/id_rsa"
}
rsync
Syncs the site to a remote server over SSH via the real rsync binary -
faster than FTP/SFTP for a full rebuild, since it only transfers what
changed. Requires rsync and ssh on the machine running bxSites.
{
"target": "rsync",
"host": "example.com",
"username": "deploy",
"remotePath": "/var/www/html",
"identityFile": "/home/me/.ssh/id_rsa"
}
netlify
{
"target": "netlify",
"siteId": "my-site-id-or-name.netlify.app",
"authTokenEnvVar": "NETLIFY_AUTH_TOKEN"
}
vercel
{
"target": "vercel",
"projectId": "my-project",
"authTokenEnvVar": "VERCEL_TOKEN"
}
cloudflare-pages
Cloudflare has no officially documented REST API for direct-upload
deploys - only its wrangler CLI. This target reverse-engineers
Wrangler's own upload flow, and needs a BLAKE3 hash implementation on the
JVM classpath that most default Java installs don't ship - see
CLI Reference and the target's own source
for the full, honest detail on this one's rough edges.
{
"target": "cloudflare-pages",
"accountId": "your-account-id",
"projectName": "my-project",
"apiTokenEnvVar": "CLOUDFLARE_API_TOKEN"
}
The package command
Prefer a plain archive over any of the targets above - attaching a build to
a GitHub release, handing it to a host that only accepts a zip upload, or
shipping it somewhere none of the pluggable targets reach?
bxSites package builds the site, then zips
it into a single file whose root is the built site's own contents (not a
wrapping site/ folder):
bxSites package
bxSites package --output=dist/my-site.zip
--output defaults to <projectRoot>/site.zip; a relative value is
resolved against the project root, and its parent directories are created
automatically if they don't already exist.
GitHub Actions (multi-version publishing)
For automatic publishing on every push, rather than a manually-run
bxSites deploy/gh-deploy, this module ships a ready-to-use GitHub
Actions workflow (.github/workflows/pages.yml) that publishes main and
development as two independently-live versions of the same site to
GitHub Pages. The rest of this guide covers that workflow, which this
repo's own docs use.
What it does
On every push to main or development that touches docs/, bxsites.yaml,
or the module's own source (theme/pipeline changes), the workflow:
- Installs BoxLang + bx-markdown
- Registers this repo as a module so
boxlang bxSites buildresolves - On any branch but
main, pointsbaseURLat.../<branch-name>/for just this build (see below) - Runs
boxlang bxSites build - Pushes
site/to thegh-pagesbranch -mainto the site root,developmentto/development/- without touching the other version
It's also available as a manual trigger (workflow_dispatch) from the
Actions tab, for a one-off republish without a new commit.
One-time setup
GitHub Pages needs to be pointed at the gh-pages branch before the
workflow can publish anything - this is a repository setting, not something
a workflow file can turn on by itself. The first successful run creates
gh-pages for you, so do this after the workflow has run at least once:
- Settings -> Pages
- Under Build and deployment -> Source, choose Deploy from a branch
- Under Branch, choose gh-pages and / (root)
After that, every matching push builds and deploys automatically. The published URL shows up under Settings -> Pages once the first deploy completes.
Publishing two versions at once
main publishes to the site root (https://<user>.github.io/<repo>/) -
treat this as the stable/released docs. development publishes to
/development/ (https://<user>.github.io/<repo>/development/) - the
latest, unreleased docs. Both stay live simultaneously: each branch's job
only pushes to gh-pages with keep_files: true and its own
destination_dir, so a development deploy never overwrites main's
content and vice versa.
main's own bxsites.yaml should have baseURL set to the site root
(https://<user>.github.io/<repo>/); the workflow overrides it for every
other branch at build time, so development's bxsites.yaml doesn't need
its own baseURL entry for this to work.
To add a third branch (e.g. a release/2.0 preview), add it to the on.push.branches
list and give it its own if: github.ref_name == '...' deploy step with
destination_dir: release-2.0 (or similar) - the pattern is the same as
development's.
Using this for your own project
Copy .github/workflows/pages.yml into your own project (adjust the
modules: line if your project needs anything beyond bx-markdown), enable
Pages as above, and pushes to main/development will publish the same
way. If you only want a single published version, delete the branch you
don't need from on.push.branches and its matching deploy step.
Serving from a project Pages sub-path
A GitHub project Pages site (as opposed to a <user>.github.io user
site) is served from https://<user>.github.io/<repo>/, not from the
domain root. Set baseURL in bxsites.yaml to that full URL so every
internal link, asset and nav entry gets the /<repo>/ prefix it needs -
and so a real sitemap.xml gets generated too:
baseURL: "https://<user>.github.io/<repo>/"
{ "baseURL": "https://<user>.github.io/<repo>/" }
See Configuration for the full breakdown of
what baseURL does. A <user>.github.io user site, or any custom domain
mapped to the site root, can leave baseURL at its default (/).
Restricting who can reach your site
There's no built-in access control here - bx-sites only ever produces a
plain static site/, and a static file has no concept of "who's asking."
bxsites.json's robots: false tells
well-behaved crawlers not to index a build (useful for a staging/preview
deploy you don't want turning up in search results), but it's a polite
request, not a lock - the URL still works for anyone who has it. If you
actually need to gate access, that has to happen in front of the static
files, at whichever host is serving them - a few common, static-friendly
options:
- Cloudflare Pages/Access - put the deployed site behind a Cloudflare Access policy (email allowlist, SSO, or a one-time PIN), no application code needed.
- Netlify - built-in password protection per site or per deploy, from the site settings alone.
- A tiny reverse-proxy (any host) - HTTP Basic Auth in front of the
static files (an
.htpasswd-style rule, or a one-file Cloudflare Worker/Netlify Edge Function) is enough for "keep search engines and randoms out," though it's not real per-user identity the way a signed-in app would have.
None of these are bx-sites features - they're host-level settings you turn
on wherever site/ ends up being served.