A client once told me deploys at their company took a full afternoon and a prayer. We replaced that with a pipeline where merging to main puts code in production in under eight minutes, safely. This is the shape of that pipeline, which I now reuse across projects.
The pipeline produces one deployable artifact: dependencies installed with --no-dev, assets compiled, version stamped. The exact bytes that passed the tests are the bytes that ship. Building on the server from git pull invites drift, and I have stopped doing it entirely.
Releases unpack into a timestamped directory; storage and .env are symlinked in; migrations run with --force; then a current symlink flips atomically and PHP-FPM reloads with OPcache reset. Users never see a mid-deploy state. Rollback is pointing current at the previous release — seconds, not minutes.
Production deploys only from main, protected by required status checks. A health-check step curls the site after the symlink flip and fails loudly (Slack webhook) if anything is off. Migrations are written to be backward-compatible for one release, which makes rollbacks genuinely safe.
Deploys should be boring. If a deploy is exciting, the pipeline has failed.