Preview Environments
FlightDesk spins up an isolated Docker container for every PR so reviewers can test changes in a real running environment without pulling the branch locally. When the PR closes, the container is torn down automatically.
How It Works
- A PR is opened on a connected repository
- FlightDesk receives the GitHub webhook
- A Docker container is created on a worker server, the repo is cloned, and your setup commands run
- Your configured processes start inside the container
- A unique HTTPS URL is assigned based on the branch name, e.g.
fix-auth.preview.flightdesk.dev - The task status moves to Preview Ready and the URL appears on the task detail page
- On every subsequent push to the branch, FlightDesk pulls the latest code and restarts the processes
- When the PR closes, the container is torn down and compute stops billing
Enabling Previews
Preview environments are opt-in per project. Enable them in Project Settings → Preview Environments with the Enable Preview Environments switch.
Setup Commands
Commands that run once inside the container before your processes start. Use these to install dependencies, run builds, generate Prisma clients, etc.
pnpm install
pnpm build:libs
pnpm prisma generate
Commands run in sequence. If any command fails, the preview is marked as errored and logs are available on the task page.
Processes
Each process defines something to run and the port it listens on. Add one row per process with Add Process, and fill in:
- Name — the identifier used in URLs and logs
- Command — the shell command that starts the process
- Port — the port it listens on inside the container
- Primary — exactly one process is primary, and its URL gets the clean subdomain with no process-name prefix
So two rows — api on 3333, and web on 4200 marked primary — give you, for a branch named fix-auth:
- Web (primary):
https://fix-auth.preview.flightdesk.dev - API:
https://api-fix-auth.preview.flightdesk.dev
Environment Variables
Store your project's env vars as encrypted secrets in Project Settings → Preview Environments → Environment Variables. They are decrypted and injected into the container at spin-up time.
FlightDesk only auto-injects NODE_ENV, NODE_OPTIONS, and NX_DAEMON. Everything else your app needs must be set as a secret — including HOST=0.0.0.0 (and VITE_HOST=0.0.0.0 for Vite), without which processes bind to localhost and the proxy returns 502s.
Use dynamic template variables so URLs automatically match the preview:
SITE_URL = {{PREVIEW_URL}}
API_URL = {{PREVIEW_URL:api}}
VITE_API_URL = {{PREVIEW_URL:api}}
CORS_ORIGIN = {{PREVIEW_URL:web}}
See Preview Environment Variables for a full reference.
Framework Guides
For step-by-step setup instructions specific to your stack:
- NestledJS App Previews — NestJS API + React frontend, two-process setup
- Shopify Theme Previews — Shopify CLI theme dev server, headless auth
Idle Stop and Wake
A preview with no real traffic for its project's idle timeout — 60 minutes by default — is stopped, which frees its RAM and stops the meter. It is not paused and it is not torn down: the container and its built state are kept.
"Real traffic" means requests from people. FlightDesk's own health probes, crawlers, gateway error responses and the wake redirect itself are all excluded, so a preview nobody is using does go to sleep.
Visiting the URL wakes it. A wake is just a container start when the built state can be reused, which is fast. If the branch has moved on since the container was built, the wake becomes a rebuild and is queued behind any build already running, so it takes as long as a build.
Working Inside a Preview
From the CLI, using the task id:
flightdesk preview logs <task-id> --follow # stream the container's logs
flightdesk preview restart <task-id> # re-run the processes, no re-clone
flightdesk preview mount <task-id> # mount /app locally over SSHFS
flightdesk preview unmount <task-id>
flightdesk preview resume <task-id> # wake a stopped preview without visiting it
flightdesk preview teardown <task-id>
flightdesk preview status <task-id>
mount needs sshfs installed locally — brew install macfuse gromgit/fuse/sshfs-mac on macOS, sudo apt install sshfs on Debian or Ubuntu. The CLI tells you if it is missing.
Manual Controls
On the task detail page you can restart a preview, tear it down early to free the compute, or start one for a task whose automatic spin-up was skipped. View Logs is there too.
Pricing
| State | Cost | |---|---| | Running (building or serving) | $0.10 / hour | | Stopped (idle, wakes on the next visit) | Free | | Torn down | $0 |
These rates apply from October 1, 2026 (UTC); before that date, usage bills at the launch rates. Build time counts as running time. The live schedule is on the pricing page, which reads it from the same source the invoices do.
Billing is per organization, never per seat: every subscription includes $5 of monthly compute credits, roughly 50 running hours. Running out of credit never stops or suspends a preview — the overage appears on the next renewal invoice.
Isolation
A preview is deliberately inert towards the outside world. Every preview container runs with FLIGHTDESK_PREVIEW=true, and an app that reads it can make itself safe the same way FlightDesk does: no scheduled jobs, no outbound writes, email to a mock provider, no payment provider.
A preview needs its own database. FlightDesk keeps a deny-list of database hosts a preview may never connect to, which includes the production API's own; a preview whose DATABASE_URL points at one refuses to boot rather than running migrations or sweeps against it. See Preview Environment Variables.
Troubleshooting
Preview is stuck on "Starting"
Open the task page and click View Logs. Common causes:
- A setup command failed — check for missing dependencies or a build error in the logs
- The process exited immediately after starting — usually a config error or crash on startup
- The health check timed out — if your app takes longer than 5 minutes to start, the preview may be marked ready anyway once the timeout passes
Preview isn't updating after a push
Pushes trigger an update within about 30 seconds. If nothing happens:
- Check that GitHub webhooks are being delivered (Settings → Organization → Integrations → GitHub)
- Confirm the push is to the same branch as the open PR
A secret isn't taking effect
Secrets are injected at spin-up time. Adding or changing a secret after the preview is running requires a Restart from the task page to take effect.