sitepilot / flight
Local development environments on Docker, with trusted HTTPS for every project.
Requires
- php: ^8.4.1
- illuminate/validation: ^13.32
- laravel-zero/foundation: ^13.23
- laravel-zero/framework: ^13.0
- laravel-zero/phar-updater: ^1.4
- symfony/yaml: ^8.1
- vlucas/phpdotenv: ^5.6
Requires (Dev)
- laravel/pint: ^1.30.4
- mockery/mockery: ^1.6.12
- pestphp/pest: ^4.7.8 || ^5.0.3
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-25 19:24:17 UTC
README
Flight is a local development environment for your web projects, built on
Docker. Describe what a project needs in a small flight.yaml file, run
flight up, and Flight starts your app and its services at a trusted HTTPS
address such as https://myapp.flght.dev, ready to work on.
- Trusted HTTPS, zero setup. Every project gets its own address with a locally trusted certificate. No ports to remember, no hosts file to edit and no browser warnings.
- Recipes for popular apps. A recipe is a ready-made setup for a kind of
project, such as Laravel or WordPress. One line in
flight.yamlgives you PHP, a database and background workers, configured to work together. - Ready on the first start. Provisioning steps install dependencies and
set your project up on
flight up, even a complete WordPress site, so a fresh checkout is ready to work on. - Bring your own Docker Compose. Run an existing compose project as it is, or extend Flight's services with compose files of your own.
- Share in seconds.
flight sharegives anyone a public URL to your local project, for a quick preview or to receive webhooks. No account needed. - Plain Docker underneath. Flight writes normal Compose files, so you can always see what runs, and every project stays isolated in its own containers.
Contents
- Installation
- How Flight Works
- Managing Projects
- Configuring Projects
- Guides
- Available Services
- Global Configuration
- AI Agents
- Troubleshooting
- Contributing
- License
Installation
Requirements
- Docker with the Compose plugin
- mkcert, to create the certificate
(
mkcert.exeon Windows when you use WSL) - PHP 8.4.1 or newer
Installing Flight
Download Flight into a directory on your PATH:
mkdir -p ~/.local/bin curl -fsSL https://github.com/sitepilot/flight/releases/latest/download/flight -o ~/.local/bin/flight chmod +x ~/.local/bin/flight
Then check that it works:
flight --version
To update Flight to the latest release later on, run:
flight self-update
Your First Project
To get started, create a project directory with a page to serve:
mkdir -p hello/public echo '<?php echo "Hello from Flight";' > hello/public/index.php cd hello
Next, add a flight.yaml file that runs the project on PHP:
app: php:8.4
Finally, start the project:
flight up
Your project is now available at https://hello.flght.dev. The address comes
from the directory name, and PHP serves the public directory by default. For
Laravel and WordPress, a recipe sets up the app and its services
for you.
Note
The first time, Flight creates a certificate and asks mkcert to trust it. Restart your browser afterwards, so it picks up the new certificate authority.
How Flight Works
Flight runs two kinds of Docker stacks:
- The Flight stack runs once for your whole machine. It contains
Traefik, a proxy that listens on ports 80 and 443 and
sends each
https://*.flght.devrequest to the right project. - A project stack runs the app and services of one project, such as PHP and MariaDB. Its app and other web services join the Flight stack's network, so Traefik can reach them, while databases stay private to the project.
The flight up command starts the Flight stack when it isn't running yet, and
then the project. Stopping a project leaves the Flight stack running for your
other projects.
Flight writes a normal Docker Compose file for every stack, so you may always look at what runs and why.
Managing Projects
Project commands may be run from anywhere inside a project. Add -v to any
command to see Docker's full output instead of a spinner, which helps when
something fails to start.
Starting and Stopping Projects
The up command starts the Flight stack when needed, then the project, and
then runs its provisioning steps:
flight up
To stop the project, use the down command. The Flight stack keeps running for
your other projects:
flight down
The restart command recreates the project's containers, for example after
changing flight.yaml:
flight restart
To start over, the destroy command removes the project's containers, volumes
and the .flight directory, after asking:
flight destroy
The open command opens your app in the browser, or another service with an
address when you name it:
flight open flight open mailpit
Flight uses the command in $BROWSER when it is set. Under WSL, it opens the
Windows browser, which trusts Flight's certificate.
Listing Projects
The list command shows your running projects and their directories:
flight list
To run a project command without changing to the project's directory, pass
the name of a running project with -p, or --project:
flight down -p myapp flight logs -p myapp -f
A stopped project isn't listed, since Docker no longer knows about it. Run
flight up in its directory to start it again.
Running Commands
The exec command runs a command in your app's container. Put the command
after --, so its options aren't read as Flight's:
flight exec -- php artisan migrate flight exec -- composer install --no-dev
To run a command in another service, pass the --service option:
flight exec --service=db -- mariadb --version
The exec command passes on the command's output and exit code, so you may
use it in scripts and pipes.
To open a shell in your app's container, or in the service you name, use the
shell command:
flight shell flight shell db
Viewing Logs
The logs command shows the logs of your app, or of the service you name. Add
-f to keep following them, and --tail to show only the latest lines:
flight logs flight logs -f --tail=100 queue
Sharing Projects
To show a running project to someone else, or to receive webhooks, use the
share command:
flight share
Flight opens a Cloudflare quick tunnel and shows
its URL, such as https://calm-river-lake.trycloudflare.com. You don't need a
Cloudflare account or anything besides Docker. The URL works until you press
Ctrl+C, and you get a new one each time. To share another service with an
address, name it: flight share mailpit.
Your app keeps receiving requests for its own hostname, such as
myapp.flght.dev. Flight replaces that hostname with the public one in
redirects, cookies and responses, so apps that only know their own URL, such as
WordPress, work unchanged. Add --direct to send the public hostname to your
app and leave responses alone.
Warning
Anyone with the URL can open the project. Quick tunnels are meant for testing: Cloudflare limits them to 200 concurrent requests, and server-sent events don't work.
Configuring Projects
Every project has a flight.yaml file in its root directory. A flight.yml
file works too; when both exist, Flight uses flight.yaml.
name: shop # optional, defaults to the directory name app: php:8.3 # what runs your app recipe: laravel # optional, sets up the app and services for Laravel services: # what your app uses, such as a database db: mariadb:11.8 provision: # optional, commands to run on `flight up` - name: Install dependencies run: composer install
| Key | Description |
|---|---|
name |
The project name, and its address: https://<name>.flght.dev. Defaults to the directory name. |
app |
What runs your app, see The App |
recipe |
A preset app and services, see Recipes |
services |
What your app uses, see Services |
provision |
Commands to run on every flight up, see Provisioning |
compose |
Your own compose files, run after Flight's, see Compose Files |
A project needs an app, services, a recipe, or compose files with
x-flight. Flight checks flight.yaml
before starting anything, and names the exact setting when something is wrong.
The App
The app key says what runs your app: its type, with a version after the
colon:
app: php:8.4 # or just `php` for the default version
To set options, write it as a mapping with a type:
app: type: php:8.4 node: "22" # Node next to PHP, e.g. to build assets hostnames: [admin]
Your app runs as the app service and is served at
https://<project>.flght.dev. The exec, shell and logs commands use it
unless you name another service. See PHP for its options. A project
that already runs with Docker Compose may keep its compose files, see
Docker Compose Projects.
Recipes
A recipe is a ready-made app and services for a kind of project, so you don't have to list them yourself:
| Recipe | Description |
|---|---|
laravel |
PHP serving the public directory, with an optional queue worker and scheduler, see Laravel |
wordpress |
PHP with WP-CLI and MariaDB, with WordPress installed for you, see WordPress |
You may change a recipe's app and services in flight.yaml. List only what
you want to be different; everything else stays as the recipe set it:
app: php:8.3 # the recipe still serves public/ recipe: laravel services: db: mariadb # adds a database next to the recipe's app
A few rules apply:
- Options are changed one by one. Lists, such as
hostnames, are replaced as a whole. - You may change and add services, but not remove the recipe's services.
- Options neither the recipe nor you set use their defaults.
- To change a recipe's app or service, you don't repeat its
type. To change its version, write the same type with another version, such asapp: php:8.3. Another type, such asmariadbfor Laravel's app, is an error, because the recipe's options wouldn't fit it.
Some recipes have options of their own, which go under the recipe's name:
recipe: wordpress: admin_user: nick
Services
Each entry under services is one container your app uses, such as a database
or a cache. You choose its name, and its type says what it runs, with a version
after the colon:
services: db: mariadb:11.8 cache: valkey # the default version
To set options, write the service as a mapping with a type:
services: db: type: mariadb:11.8 database: shop
Services reach each other by name, so from your app, the service above is at
the host db. The name app belongs to your app.
A service may also be an extra PHP container next to your app, served at its own address, or a service from your own compose files, see Docker Compose Projects:
services: legacy: php:8.1 # https://<project>-legacy.flght.dev
See Available Services for every service and its options.
Workers
Workers are background processes of your app, such as a queue worker. Each runs in a container of its own, on the app's image, with the same files and settings, so it always matches your app:
app: type: php:8.4 workers: queue: php artisan queue:work scheduler: php artisan schedule:work
A worker goes by its own name, such as queue, so each name may be used once
in a project, by a service or a worker. It starts and stops with the project,
restarts when it stops, and works with flight logs queue and
flight exec --service=queue.
Note
Give a worker a command that keeps running, such as schedule:work rather
than schedule:run.
Hostnames
Flight gives your app and every other web service an address under
flght.dev. Your app gets https://<project>.flght.dev, and any other web
service https://<project>-<service>.flght.dev. For a project called shop
with an extra PHP service legacy, that's shop.flght.dev and
shop-legacy.flght.dev.
To answer on more addresses, for example for a multisite or an admin panel,
add hostnames:
app: type: php:8.4 hostnames: [admin, api] # also admin.flght.dev and api.flght.dev
Each hostname is one subdomain, such as admin or my-shop, because the
certificate covers one level under flght.dev. Two services can't share a
hostname.
Provisioning
Provisioning steps are commands that set your project up, such as installing
dependencies. They run inside the project's containers on every flight up,
right after the project has started. Steps from a recipe run first, then
yours:
provision: - name: Install dependencies run: composer install unless: test -d vendor
| Key | Description |
|---|---|
name |
A short description, shown while the step runs |
service |
Optional. The service to run the command in; defaults to your app |
run |
The shell command to run |
unless |
Optional. A check command; when it succeeds, the step is skipped |
dir |
Optional. The directory to run in, see below |
env |
Optional. Secret variables the step needs, see Secrets |
Because steps run on every flight up, each one should be safe to repeat. Add
an unless check to skip a step once its work is done, or use a command that's
harmless to run again. When a step fails, flight up stops and shows what went
wrong.
A step runs in the service's working directory, which for app is the project
directory. Use dir to run it somewhere else:
provision: - name: Install tool dependencies dir: tools run: composer install
Secrets
License keys and tokens don't belong in flight.yaml, because you commit that
file. Instead, list the variables a step needs under env, and keep their
values somewhere private:
provision: - name: Install dependencies env: [COMPOSER_AUTH] # Composer reads this for private packages run: composer install
Flight looks for each variable in three places, and uses the first it finds:
- Your shell, e.g.
export COMPOSER_AUTH=... - The project's
.envfile, which Docker Compose reads too - The
~/.config/flight/.envfile, for all your projects
# ~/.config/flight/.env COMPOSER_AUTH='{"github-oauth": {"github.com": "your-token"}}'
If a variable can't be found, flight up stops before starting anything and
tells you where to set it. Inside the step, use the variable as ${NAME}, or
let a tool read it, as Composer does here. Only the variables a step lists are
read.
Warning
Keep the project's .env file out of Git. flight up warns when a step
reads a secret from a .env file that Git doesn't ignore.
Compose Files
The compose key lists compose files of your own. Flight runs its generated
.flight/compose.yaml first, and your files after it in this order, as with
docker compose -f. So your files may add services and override anything
Flight generates. For example, to mount an extra directory:
app: php:8.4 compose: - path: compose.override.yml required: false
# compose.override.yml services: app: volumes: - ./packages/my-package:/var/www/html/vendor/acme/my-package
A file with required: false is skipped when it doesn't exist, so it may be a
personal, git-ignored override. As with docker compose -f, relative paths and
.env resolve from the directory of the first file. The header of
.flight/compose.yaml lists the files in the order Flight runs them.
A service with an x-flight block in your files gets an address too, and
your files may define a whole project, see
Docker Compose Projects.
The .flight Directory
Flight keeps the files it generates for a project in a .flight directory,
which it hides from Git for you:
| Path | Description |
|---|---|
compose.yaml |
The generated Docker Compose file; don't edit it, add compose files instead |
<service>/build/ |
Files a service's image is built from |
<service>/data/ |
What a service keeps, such as WordPress when you develop a theme |
Note
Tools that scan your whole repository, such as linters, may need .flight
added to their ignore list.
Guides
Laravel
To run a Laravel app with a database, add a flight.yaml file to the project:
recipe: laravel services: db: mariadb
Then point Laravel's .env file at the database:
DB_CONNECTION=mariadb DB_HOST=db DB_DATABASE=flight DB_USERNAME=flight DB_PASSWORD=flight
Run flight up and open https://<project>.flght.dev. Artisan and Composer
run in the container:
flight exec -- php artisan migrate flight exec -- composer test
For queued jobs and scheduled tasks, turn on the recipe's queue worker and scheduler. They run as workers of your app, start and stop with it, and pick up code changes by themselves:
recipe: laravel: queue: true scheduler: true
You may follow their output with flight logs -f queue or
flight logs -f scheduler.
| Recipe Option | Default | Description |
|---|---|---|
queue |
false |
Adds a queue worker running php artisan queue:listen |
scheduler |
false |
Adds a scheduler worker running php artisan schedule:work |
Note
Run Vite on your machine with npm run dev, where it watches files fastest.
Set APP_URL=https://<project>.flght.dev in .env, so Vite lets the site
load its scripts.
WordPress
To create a WordPress site, add a flight.yaml file to an empty directory:
recipe: wordpress
Then run flight up. The first time, Flight downloads WordPress into the
directory, creates wp-config.php and installs the site. Log in at
https://<project>.flght.dev/wp-admin with admin / admin. Later runs skip
these steps, so your site is left as it is.
| Recipe Option | Default | Description |
|---|---|---|
title |
the project name | The site title |
admin_user |
admin |
The administrator's username |
admin_password |
admin |
The administrator's password |
admin_email |
admin@flght.dev |
The administrator's email address |
WP-CLI is installed in your app's container, together with the MariaDB client for its database commands:
flight exec -- wp plugin list flight exec -- wp db export backup.sql
WordPress Themes and Plugins
When your repository is a theme or a plugin, WordPress itself should stay out
of it. Tell Flight where your project belongs inside WordPress with
project_path. Flight then keeps WordPress in .flight/app/data, and mounts
your repository into it:
app: project_path: wp-content/themes/my-theme # or wp-content/plugins/my-plugin recipe: wordpress provision: - name: Activate theme run: wp theme activate my-theme unless: wp theme is-active my-theme
Run flight up, and your theme is installed and active in a fresh WordPress
site. You may browse the WordPress files in .flight/app/data.
Docker Compose Projects
To run a project that already has compose files, give each service that should
get an address an x-flight block with its origin: the service name and the
port it listens on. Docker Compose ignores x- keys, so your files keep working
without Flight:
# compose.yml services: app: image: serversideup/php:8.4-fpm-nginx volumes: - ./:/var/www/html x-flight: origin: https://app:8443 mailpit: image: axllent/mailpit x-flight: origin: http://mailpit:8025
Then list your files under compose in flight.yaml. For such a project, that
may be all it holds:
compose: - compose.yml - path: compose.override.yml required: false
The service named app is the app. To make another service the app, add
app: true to its x-flight. Flight reads x-flight through Docker Compose,
so variables such as ${APP_PORT}, later files, extends and include work
as for any other setting. See Compose for all options.
Then start it with Flight:
flight up
Your app is now served at https://<project>.flght.dev, and Mailpit at
https://<project>-mailpit.flght.dev. Your files run after Flight's generated
file, as described in Compose Files, also when they're in a
directory such as .docker.
The exec, shell and logs commands use the app unless you name another
service from your files:
flight logs -f db
Flight runs the project under the same name as Docker Compose, so
docker compose keeps working on the same containers and volumes. A plain
docker compose up leaves out the Flight address; run flight up to get it
back.
Note
flight destroy removes your files' volumes too, as
docker compose down --volumes does.
Available Services
PHP
Runs PHP with a web server, based on
serversideup/php, usually
as your app. Your project is available in the container at
/var/www/html.
app: type: php:8.3 extensions: [intl]
| Option | Default | Description |
|---|---|---|
type |
php |
With a version after the colon: php:8.1 to php:8.5. The default is 8.4. |
server |
fpm-nginx |
fpm-nginx, fpm-apache or frankenphp |
webroot |
public |
The directory the web server serves; . for the root |
extensions |
none | Extra PHP extensions, such as [mysqli, gd] |
packages |
none | Extra Debian packages, such as [git] |
wp_cli |
false |
Installs WP-CLI as wp, with less for its help pages |
node |
none | Installs Node.js and npm of this version, such as "22", next to PHP |
workers |
none | Background processes on the same image, see Workers |
access_log |
false |
Logs every request; errors are always logged |
project_path |
. |
Where your project goes inside the app, such as modules/my-module. The app itself is then kept in .flight/app/data. |
hostnames |
none | Extra addresses, see Hostnames |
The container serves HTTPS itself, behind Flight's proxy, so apps such as
Laravel and WordPress see an HTTPS request and create https:// links without
any configuration. The image is built with your user and group ID, so files the
container creates in your project belong to you.
Note
Quote the node version, so "20.10" isn't read as the number 20.1.
MariaDB
Runs a MariaDB database. Other services connect to it at
its name, such as db, on port 3306. Its data is kept in a Docker volume, so
it survives flight down.
services: db: mariadb:11.8
| Option | Default | Description |
|---|---|---|
type |
mariadb |
With a version after the colon: 10.6, 10.11, 11.4 or 11.8. The default is 11.8. |
database |
flight |
The database created on the first start |
user |
flight |
A user with access to that database |
password |
flight |
The password for that user and for root |
The database, user and password are only set on the very first start. To start
over with an empty database, run flight destroy and then flight up.
Valkey
Runs Valkey, a Redis-compatible store for caches, queues
and sessions. Other services connect to it at its name, such as cache, on
port 6379. Its data is kept in a Docker volume, so it survives flight down.
services: cache: valkey:9.1
| Option | Default | Description |
|---|---|---|
type |
valkey |
With a version after the colon: 7.2, 8.0, 8.1, 9.0 or 9.1. The default is 9.1. |
Apps that talk to Redis work unchanged. In Laravel, for example, set
REDIS_HOST=cache.
Compose
Serves a service from your own compose files, listed under compose. You
don't add it to flight.yaml: give the service an x-flight block in your
compose file instead. Flight adds the flight network and Traefik labels to
the service, and leaves the rest to your files. See
Docker Compose Projects.
services: web: image: nginx x-flight: app: true origin: http://web:80
| Option | Default | Description |
|---|---|---|
origin |
The URL the proxy connects to: this service's name and container port. Use https:// when the container serves HTTPS itself; its self-signed certificate is accepted. |
|
hostnames |
none | Extra addresses, see Hostnames |
app |
false |
Makes this service the app, served at https://<project>.flght.dev. The default is true for a service named app. |
Traefik
The proxy in the Flight stack. You don't add it to a project; it's configured
in the global configuration. Its dashboard is at
https://traefik.flght.dev.
| Option | Default | Description |
|---|---|---|
http_port |
80 |
The port on your machine for HTTP |
https_port |
443 |
The port on your machine for HTTPS |
docker_socket |
/var/run/docker.sock |
The Docker socket Traefik watches |
Global Configuration
Settings that apply to all projects live in ~/.config/flight/config.yaml. A
config.yml file works too; when both exist, Flight uses config.yaml. To
open it in your editor, run:
flight stack:config
domain: flght.dev network: flight services: traefik: http_port: 8080
| Key | Default | Description |
|---|---|---|
domain |
flght.dev |
The domain your projects are served under |
network |
flight |
The Docker network projects join |
services |
Options for the Flight stack's services, such as Traefik | |
compose |
Your own compose files, run after Flight's, see Adding Services to the Flight Stack |
After changing it, restart the Flight stack:
flight stack:restart
Every *.<domain> address must point to 127.0.0.1. After changing domain,
create a matching certificate:
flight stack:secure
The Flight stack is also started, stopped and recreated by the stack:up,
stack:down and stack:restart commands. The directory holds your own files,
and a .flight directory with what Flight generates, just like a project:
| Path | Description |
|---|---|
config.yaml |
The settings above |
.env |
Secrets for all your projects |
traefik/ |
Your own Traefik configuration files, loaded automatically |
.flight/compose.yaml |
The generated Compose file; don't edit it |
.flight/certs/ |
The certificate, managed by Flight |
.flight/share/ |
The files the flight share image is built from |
Adding Services to the Flight Stack
Services in your own compose files start and stop with the Flight stack, as
compose files do for a project. List them under compose in
config.yaml, with paths relative to ~/.config/flight, and give a service an
x-flight block for an address. For example, to add
Mailpit at https://mailpit.flght.dev:
# ~/.config/flight/config.yaml compose: - mailpit.yaml
# ~/.config/flight/mailpit.yaml services: mailpit: image: axllent/mailpit x-flight: origin: http://mailpit:8025
In these files, you may use the FLIGHT_DOMAIN, FLIGHT_NETWORK,
FLIGHT_HTTP_PORT, FLIGHT_HTTPS_PORT and FLIGHT_DOCKER_SOCK variables.
Custom Traefik Configuration
Any .yaml or .yml file in ~/.config/flight/traefik is loaded by Traefik
right away, without a restart. You may use it for middlewares, or to route to
something outside Docker.
AI Agents
Flight comes with an agent skill that teaches AI coding agents, such as Codex, Cursor, GitHub Copilot and Claude Code, how to run and configure your project with Flight. To add it to a project, run:
flight skill
Flight writes the skill to .agents/skills/flight, where most agents look
for it. Claude Code only reads .claude/skills, so Flight offers to link the
skill there too. Commit both, so everyone working on the project gets it. To
install the skill for all your projects instead, in your home directory, add
--global.
Run the command again after updating Flight, to update the skill.
Troubleshooting
Something doesn't start. Run the command again with -v to see Docker's
full output.
The browser warns about the certificate. Restart your browser after the
first start, so it picks up mkcert's certificate authority. If that doesn't
help, run flight stack:secure.
Port 80 or 443 is already in use. Another program is using it. Stop that program, or pick other ports in the global configuration:
services: traefik: http_port: 8080 https_port: 8443
Your settings are rejected. Flight checks flight.yaml and config.yaml
before starting anything. The error names the exact setting, such as
app.type, and what it expects.
You want to try something without touching your setup. Point Flight at another configuration directory:
FLIGHT_CONFIG_DIR=/tmp/flight-test flight stack:up
Contributing
To work on Flight, clone the repository and install its dependencies. The
./flight script runs straight from the checkout:
git clone git@github.com:sitepilot/flight.git
cd flight
composer install
./flight stack:up
Run the tests with Pest, and format the code with Pint:
composer test
composer lint
To build a binary, run:
php flight app:build flight --build-version=1.0.0
The self-update command only works for a downloaded release. In a checkout,
pull the repository instead.
To release a new version, publish a release on GitHub with a tag such as
v1.0.0. The Release workflow builds the binary and attaches it to the
release.
License
Flight is open-source software licensed under the MIT license.