Skip to main content
Laravel, shipping fast.

Chapter 14

DevOps & Infrastructure

Julian Beaujardin

Your API can have perfectly typed DTOs, a full Pest suite, and Response Wrappers that never drift, and none of it matters until the code is running somewhere your users can reach it. Deployment is where theory meets reality. The FormRequest that validated cleanly on your laptop has to survive a migration running against a database that's still serving live traffic. The queue worker that processed jobs correctly in your test suite has to keep processing jobs while you ship the exact code that changes the job's class definition. The health check from Chapter 1 has to actually mean something when your host is deciding whether the new release is safe to receive traffic.

This chapter covers what shipping looks like once you're past the pull request: a pipeline that blocks bad code before a server ever sees it, a deploy that doesn't drop requests while it happens, migrations that are safe to run against a schema still being read by the previous release, a rollback plan you actually believe in at 2am, secrets that live outside your repository, and provisioning you can repeat without reading your own notes from six months ago.

A deploy you have to hold your breath through is a deploy you haven't finished designing.

A pipeline that refuses bad code

Why put checks in front of a deploy instead of trusting a developer to run them locally first? Because developers forget, and CI doesn't.

Every current app in this fleet, api-server, api-license, api-ai, merchants, api-infrastructure, defines the same three composer scripts, format, check, test. Four of the five run the identical commands underneath. Merchants is the one exception: its test script runs config:clear first, then php artisan test instead of calling ./vendor/bin/pest directly, because it needs a clean cached config before the suite boots. Same job, same name, one app wired slightly differently for a reason specific to that app.

// composer.json
{
    "scripts": {
        "format": [
            "./vendor/bin/pint"
        ],
        "check": [
            "./vendor/bin/phpstan analyse --memory-limit=2G"
        ],
        "test": [
            "./vendor/bin/pest"
        ]
    }
}

format runs Pint, Laravel's opinionated code formatter, and fixes style issues automatically. check runs PHPStan, the static analysis tool this book has used since Chapter 1. It doesn't fix anything, it just refuses to pass if your types don't add up. Four of the five apps configure it at level 9, the strictest setting; merchants runs at level 5. Even inside a fleet this disciplined about sharing one command name, the number behind that command isn't automatically the same everywhere, and nobody enforces that it should be. test runs the Pest suite.

A pipeline, whatever runs it, GitHub Actions, GitLab CI, Buildkite, doesn't need to reinvent this. It runs the same three commands a developer runs locally, with one adjustment: composer format -- --test instead of a bare composer format, Pint's actual flag for failing on style violations instead of silently rewriting the files a CI runner has no business committing back, then composer check, then composer test. If any one of them exits non-zero, the build is red and nothing downstream gets a chance to run.

That last part is the whole point. A developer who forgets to run PHPStan before pushing doesn't get a second chance to remember. The pipeline remembers for them. This is the same discipline this book has argued for since Chapter 1, just applied to the boundary between your repository and production instead of the boundary between a request and your controller: validate at the edge, fail loudly, and never let bad input travel further than it has to. Here, the "bad input" is code that doesn't type-check or doesn't pass its own tests.