Infrastructure Generators
bext ships four CLI generators that produce infrastructure config tailored to your project. They read bext.config.toml, detect the framework from ambient file markers (like astro.config.mjs, Gemfile, manage.py), and emit a sensible default that you can commit and edit.
All four generators default to stdout so you can pipe them directly to a file, or pass --out <path> to write the file yourself.
bext dockerfile gen
Emits a multi-stage Dockerfile tailored to the detected framework.
bext dockerfile gen > Dockerfile
# or
bext dockerfile gen --out Dockerfile
The generated Dockerfile has three stages:
1. bext-base — the official bext runtime image (ghcr.io/bextdev/bext:latest by default; override with --bext-image)
2. framework-build — the framework's toolchain: oven/bun:1 for Astro / SvelteKit / SolidStart / Qwik / PRISM, ruby:3.3-slim for Rails, python:3.12-slim for Django, composer:2 + php:8.3-fpm-alpine for Laravel, elixir:1.16-alpine for Phoenix
3. runtime — final minimal image that copies the build output, sets a HEALTHCHECK against bext health, and runs bext serve
For projects where no framework is detected, the generator skips the framework-build stage and emits a minimal runtime that just copies the project tree and runs bext serve.
Flags
| Flag | Default | Purpose |
|---|---|---|
--dir <path> |
. |
Project root to read |
--out <path> |
stdout | Write Dockerfile to this path |
--bext-image <tag> |
ghcr.io/bextdev/bext:latest |
Override the base image |
bext ci gen
Emits a CI workflow for a supported platform. Wave 1 ships GitHub Actions; GitLab, CircleCI, Cloudflare, Fly, and Render return a TODO(E13 wave 2) error so the CLI surface is stable for later additions.
bext ci gen --platform github > .github/workflows/bext.yml
# with a deploy target
bext ci gen --platform github --deploy-target self-hosted --out .github/workflows/bext.yml
# no deploy step, just build + test
bext ci gen --platform github --deploy-target none
The default GitHub workflow has:
- A build job that checks out the repo, installs the Rust toolchain, installs bext via the curl-installer, and runs bext check, bext build, and bext test
- A deploy job that runs only on pushes to main/master, SSHes into DEPLOY_HOST using secrets, and runs bext upgrade + systemctl reload bext
Flags
| Flag | Default | Purpose |
|---|---|---|
--dir <path> |
. |
Project root to read |
--platform <name> |
github |
CI platform (wave 1: github) |
--deploy-target <kind> |
self-hosted |
self-hosted or none |
--out <path> |
stdout | Write workflow to this path |
bext systemd gen
Emits a bext.service unit file (and optionally a companion bext.socket unit when --socket-activation is passed).
bext systemd gen > bext.service
# with socket activation
bext systemd gen --socket-activation > units.txt
# custom user + working directory
bext systemd gen --user www-data --working-dir /srv/bext
The output uses a sentinel ### FILE: bext.service header to separate each unit in the stream. Split on that header if you piped both units to a single file.
The default unit uses Type=notify, sets hardening flags (NoNewPrivileges, ProtectSystem=strict, PrivateTmp, PrivateDevices), and restarts on failure with a 2-second back-off.
With --socket-activation, the service gets Requires=bext.socket + Sockets=bext.socket and the socket unit listens on 0.0.0.0:80 + 0.0.0.0:443 with ReusePort=true. This is the shape the zero-downtime binary upgrade path expects — see Zero-Downtime Upgrades.
Flags
| Flag | Default | Purpose |
|---|---|---|
--dir <path> |
. |
Project root to read |
--socket-activation |
off | Also emit bext.socket and wire it into the service |
--user <user> |
bext |
Unix user the service runs as |
--working-dir <path> |
/opt/bext |
WorkingDirectory for the service |
--out <path> |
stdout | Write units to this path |
bext export docker-compose
The reverse of bext import: walk your bext.config.toml and declared plugins, then emit a docker-compose.yaml suitable for local development.
bext export docker-compose > docker-compose.yaml
The bext service is always present. Backend services are auto-added from plugin naming conventions:
| Plugin name contains | Service added | Env var wired |
|---|---|---|
-pg, -postgres, -pgsql |
postgres:16-alpine |
DATABASE_URL |
-redis |
redis:7-alpine |
REDIS_URL |
-meili, meilisearch |
getmeili/meilisearch:v1.7 |
MEILI_URL |
So a project that declares @bext/session-redis and @bext/locking-pg gets both redis and postgres services with the matching env vars wired into the bext service. Volumes and a shared bext-net network are emitted for every backend.
A richer version that reads requires_capabilities from plugin manifests is a wave-2 item; for v1 the naming convention is authoritative.
Flags
| Flag | Default | Purpose |
|---|---|---|
--dir <path> |
. |
Project root to read |
--out <path> |
stdout | Write compose to this path |
--bext-image <tag> |
ghcr.io/bextdev/bext:latest |
Override the image used by the bext service |
Regeneration
All four generators are pure functions of the project state — running them twice produces identical output. Re-run them after bumping bext or adding plugins and commit the diff; that's the intended review loop.
They also have no side effects beyond writing the target file. They never modify bext.config.toml, never fetch anything from the network, and never touch the plugin cache. This makes them safe to run inside CI pipelines, pre-commit hooks, or scripts.
Framework detection
The generators recognise:
- Astro — astro.config.*
- SvelteKit — svelte.config.*
- SolidStart — solid.config.*
- Qwik — qwik.config.*
- Rails — Gemfile with a gem 'rails' line
- Django — manage.py
- Laravel — composer.json with "laravel/framework"
- Phoenix — mix.exs with :phoenix
- PRISM (React) — package.json with a "react" dep
Detection is a thin file-marker scan, not a full project parse. If you hit a project the generators misidentify, please open an issue.