serversideup/spin
Spin is a Docker-based local development environment for Laravel that gets projects running quickly with preconfigured PHP, database, and caching services, so you can develop without installing PHP, Composer, or Node locally.
Spin is a thin, dependency-free Bash wrapper around docker compose plus a handful of container-run tooling commands. The mental model to adopt on day one: anything you already know about Compose still applies — Spin just picks the right compose files, sets --remove-orphans, pulls images, and maps your UID into containers.
tools/install.sh) in the project root and commit the resulting spin executable. This is the single highest-value habit: every teammate and your CI runner executes the exact same version, so "works on my machine" stops being a Docker-version conversation.COMPOSE_FILE=docker-compose.yml:docker-compose.dev.yml. Before you run anything, make sure the base file describes the production-shaped stack (image tags, no bind mounts, no debug ports) and the overlay adds local ergonomics (volume mounts, xdebug, mailpit, vite port). If you have a single fat docker-compose.yml with local-only settings baked in, you've defeated the point.spin up. That's it. Then spin exec php composer install, spin exec php php artisan migrate, spin exec php php artisan serve.spin help, then docs/content/docs/9.command-reference/ for per-command flags, and conf/spin.example.conf in the repo for the full set of tunables (SPIN_ENV, image overrides, update-check interval).The canonical first use case — a Laravel app with no local PHP, Composer, or Node installed — looks like:
spin up
spin exec -T php composer install
spin exec -T php php artisan key:generate
spin exec -T php php artisan migrate --seed
spin exec -T node npm ci && spin exec -T node npm run dev
spin logs -f php
Use SPIN_ENV as your environment switch, not ad-hoc flags. Spin resolves docker-compose.$SPIN_ENV.yml, defaulting to dev. That gives you a clean ladder of overlays without inventing new tooling:
spin up # docker-compose.yml + docker-compose.dev.yml
SPIN_ENV=testing spin up # docker-compose.yml + docker-compose.testing.yml
spin deploy production # same files, production overlay, swarm target
Keep overlays small and additive. A typical set: dev (bind mounts, xdebug, exposed DB port), testing (ephemeral DB, CI-safe, no host ports), staging/production (image tags only, resource limits, logging driver).
Prefer spin exec over spin run during active development. spin exec reuses the live container and is near-instant. spin run spins up a fresh one-off container, removes it afterwards, and — importantly — does not start dependencies, so spin run php php artisan migrate against a stopped DB will fail. Reserve spin run for when the stack is down or you deliberately want isolated state / different env vars.
Pass the -T flag in any non-interactive context. Anywhere stdin isn't a real TTY — CI, $(...), Makefile recipes, subprocesses spawned by scripts, or an AI agent — Compose will error with "the input device is not a TTY". Make it reflexive:
spin exec -T php php artisan test
spin run -T php composer install --no-interaction
Treat Spin as your CI entry point too. Because it's just a Bash script with no runtime dependency beyond Docker, the same spin up / spin exec -T sequence you use locally works in GitHub Actions:
- run: cp .env.example .env
- run: spin up
- run: spin exec -T php php artisan test
- run: spin down
Run server-side tooling in containers instead of installing it. spin provision shells out to serversideup/ansible-core; spin gh and spin node similarly. This is the feature that quietly saves the most time — you get Ansible and the GitHub CLI in your deploy pipeline without adding a Python/Node toolchain to anyone's laptop or CI image.
Configure via environment variables, not by editing bin/spin. Every knob is a VAR=${VAR:-default}; override in the shell, in CI secrets, or in the project config. Image overrides (SPIN_PHP_IMAGE, SPIN_NODE_IMAGE, COMPOSE_CMD for Podman/docker-compose) are the intended extension points.
Fork only for genuinely new verbs. If you need a real new command, the pattern is explicit: add lib/actions/thing.sh exporting action_thing(), filter Spin's own args with filter_out_spin_arguments "$@" and consume ${SPIN_FILTERED_ARGS[@]}, then add a case branch in bin/spin. Respect the Bash 3.2 constraint (no declare -A, no mapfile, no ${var,,}) or macOS users break immediately.
spin up pulls images by default. There's a cache to keep this fast, but on a cold machine, a metered connection, or an air-gapped runner it's a surprise. Use --skip-pull to trust your local images and --force-pull to bust a stale cache when a :latest tag moved underneath you.
SPIN_ENV is sticky and silent. Forgetting SPIN_ENV=testing means your "test run" hammers the dev database, and forgetting it before spin deploy means you're deploying the dev overlay. Bake it into a Makefile target or a just recipe rather than typing it from memory.
spin up/down always add --remove-orphans. Good hygiene, but it means editing a service name in the compose file will silently reap the old container — including its anonymous volumes — on the next up. Don't stash state you care about in unreferenced containers.
Tool wrappers and Compose commands behave differently as file owners. The spin php / spin node wrappers run with -u "${SPIN_USER_ID}:${SPIN_GROUP_ID}" so bind-mounted files land owned by you. But spin exec php ... goes through Compose's service definition, so ownership is whatever the image declares. If you get root-owned storage/logs or vendor/, check which of the two paths created them, and prefer setting user: in the compose service over fighting it later.
WSL2 users: run from the Linux filesystem. /mnt/c/... bind mounts are slow and have unreliable permission semantics. Clone into ~/code and everything — file ownership, polling, Composer speed — improves.
Debug with SPIN_DEBUG=true. It prints the exact command being executed, which resolves 90% of "why did it run that compose file?" questions in seconds. When that's not enough, spin logs is a straight Compose pass-through, so all the usual -f, --tail, -t flags work.
The self-update check is scheduled, not automatic-on-every-run. Spin writes a timestamp into a cache file and only prompts after AUTO_UPDATE_INTERVAL_IN_DAYS has elapsed. If a teammate's behavior diverges from yours, diff the committed spin / lib/ before debugging anything else.
Don't build a DSL on top of it. Spin deliberately exposes Docker and Compose syntax rather than inventing abstractions. When you're tempted to add a spin migrate-all wrapper, ask whether a compose overlay plus a documented spin exec incantation in the README would do — it usually will, and it stays debuggable.
Production commands deserve a guardrail. spin deploy and spin provision reach real hosts. Put them behind an explicit environment argument in your runbook, require the production overlay's approval step in CI, and never let a spin up-adjacent script fall through to them.
License note: GPL-3.0. Consuming Spin in-house is unproblematic; redistributing a modified Spin (e.g. shipping it inside a commercial product) carries source-availability obligations. Keep local customization in your own scripts and compose files rather than forking the binary unless you're comfortable with that.
How can I help you explore Laravel packages today?