PHPackages                             cuonggt/bosun - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [DevOps &amp; Deployment](/categories/devops)
4. /
5. cuonggt/bosun

ActiveLibrary[DevOps &amp; Deployment](/categories/devops)

cuonggt/bosun
=============

Provision servers and deploy Laravel applications with zero downtime, straight from artisan.

12PHP

Since Jul 11Pushed 1mo agoCompare

[ Source](https://github.com/cuonggt/bosun)[ Packagist](https://packagist.org/packages/cuonggt/bosun)[ RSS](/packages/cuonggt-bosun/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependenciesVersions (1)Used By (0)

Bosun
=====

[](#bosun)

> A bosun (boatswain) is the crew member who provisions, maintains, and readies the ship before it sails. This package does the same for your Laravel app.

Provision servers and deploy Laravel applications with zero downtime — straight from `artisan`.

This package gives you two commands:

CommandWhat it does`php artisan setup`Provisions a fresh Ubuntu server: PHP-FPM, Nginx, Composer, Node, Redis, Supervisor, Certbot, a firewall and a non-root deploy user.`php artisan deploy`Deploys your app with zero downtime using timestamped releases, shared `.env`/`storage`, atomic symlink swaps and automatic rollback-ready release pruning.`php artisan rollback`Instantly reverts to the previous release (or a specific one) with the same atomic symlink swap.`php artisan secure`Enables HTTPS by obtaining a Let's Encrypt certificate (Certbot) and adding an HTTP→HTTPS redirect. Certs auto-renew.`php artisan status`Shows the deployed release and commit, `.env` presence, service health, queue workers and disk usage.`php artisan logs`Tails a log from the server — Laravel, Nginx, queue worker or PHP-FPM — with optional live `--follow`.`php artisan ssh`Opens an interactive SSH shell on the server, dropped into the current release.It talks to your servers over SSH using [phpseclib](https://phpseclib.com/) — so provisioning and deploys need no local `ssh` binary, and the whole thing is unit-tested against a fake connection. (The one exception is `php artisan ssh`, which shells out to your real `ssh` client for an interactive terminal.)

---

Requirements
------------

[](#requirements)

- PHP 8.1+
- Laravel 10, 11, 12 or 13
- A target server running **Ubuntu 22.04 or 24.04** that you can reach over SSH as `root` (most cloud providers give you this on a fresh box). `setup` refuses to run on anything else.

Installation
------------

[](#installation)

```
composer require cuonggt/bosun
```

The service provider is auto-discovered. Publish the config file:

```
php artisan vendor:publish --tag=bosun-config
```

This creates `config/bosun.php`.

Configuration
-------------

[](#configuration)

Everything is driven by `config/bosun.php`, which reads from your `.env`. A minimal setup:

```
DEPLOY_HOST=203.0.113.10
DEPLOY_USER=deployer
DEPLOY_KEY=~/.ssh/id_rsa
DEPLOY_DOMAIN=example.com
DEPLOY_REPOSITORY=git@github.com:acme/app.git
DEPLOY_BRANCH=main
DEPLOY_PATH=/home/deployer/app
```

Each server is an entry under `servers` in the config file, so you can define `production`, `staging`, and more. Key options:

OptionDescriptionDefault`host` / `port`Where to connect.— / `22``username`The **deploy user** — created during `setup`, connected as during `deploy`.`deployer``key` / `passphrase` / `password`Auth. A readable key file wins; otherwise the password is used.`~/.ssh/id_rsa``domain`Server name for the generated Nginx site.—`ssl_email`Email for the Let's Encrypt certificate (`secure`).—`deploy_path`Where the app lives on the server. `{application}` is substituted.`/home/deployer/{application}``php` / `node`Stack to provision.`8.3` / `20`App-wide options (outside `servers`): `repository`, `branch`, `database`, `cloudflare`, `shared_files`, `shared_dirs`, `keep_releases`, `build_assets`, `queue`, and `hooks`. Set `cloudflare` to `true` if the site sits behind Cloudflare, so Nginx logs the real visitor IP.

Provisioning a server
---------------------

[](#provisioning-a-server)

Point the package at a fresh server and run:

```
php artisan setup production
```

By default it connects as `root` (override with `--user`). It will:

1. Update **and upgrade** apt, then install base tooling (a build toolchain for native npm/pecl modules, git, `jq`, `cron`, etc.).
2. **Harden SSH** to key-only authentication (password login is disabled).
3. Install PHP (with the common Laravel extensions, including the MySQL/Postgres/SQLite PDO drivers), Composer, Nginx, Redis, Supervisor, Node and Certbot.
4. Create the unprivileged **deploy user**, authorizing the same SSH key you connected with (add another with `--key`).
5. Generate an SSH **deploy key** for that user and trust your Git host, so it can clone a private repo (see below).
6. Grant that user *passwordless* permission to reload PHP-FPM/Nginx and control Supervisor — nothing more.
7. Configure the firewall (UFW: SSH + HTTP/HTTPS), install **fail2ban**, and enable **automatic security updates**.
8. Lay out the deploy directory, write the Nginx site and the Supervisor queue worker, and register **Laravel's scheduler** (`schedule:run` every minute via `/etc/cron.d`), then start everything. When a domain is set, a catch-all server rejects requests that don't match it (so the app never answers on the bare IP or a spoofed `Host`).

Every step is **idempotent**, so re-running `setup` to add an extension or change a setting is safe. On a server bosun has already provisioned, it tells you when and asks before proceeding (skipped with `--no-interaction`).

### Private repositories

[](#private-repositories)

Use an **SSH** repository URL (`git@github.com:you/app.git`). At the end of `setup`, bosun prints the deploy user's public key:

```
Add this read-only deploy key to your Git repository, then deploy:

  ssh-ed25519 AAAA… bosun-app@203.0.113.10

```

Register it as a **read-only deploy key** (GitHub: *Settings → Deploy keys*; GitLab: *Settings → Repository → Deploy keys*), then `php artisan deploy`. The key is per-server, read-only, and regenerated only if absent — so re-running `setup` never invalidates a key you've already registered.

> **Databases are out of scope** — with one exception. bosun installs the MySQL/Postgres/SQLite PDO drivers but does not provision a database *server*; point your app at a managed or external database via `DB_*` in the server's `shared/.env`. For **SQLite**, set `database` to `"sqlite"` (or `DEPLOY_DATABASE=sqlite`) and bosun keeps `database/database.sqlite` in `shared/` — symlinked into each release and writable by the web server — so it persists across deploys.

```
php artisan setup production --user=root --key=~/.ssh/deploy_key.pub
```

HTTPS
-----

[](#https)

Certbot is installed during `setup`. Once your domain's **DNS points at the server** (the Let's Encrypt challenge needs it), turn on HTTPS:

```
php artisan secure production            # obtains the cert + enables HTTP→HTTPS redirect
php artisan secure production --www      # also cover www.
```

Set `ssl_email` (or `DEPLOY_SSL_EMAIL`) first — Let's Encrypt requires it for expiry notices. The command is idempotent, and Certbot's timer renews the certificate automatically thereafter. Re-running `setup` rewrites the Nginx site but **re-applies an existing certificate**, so it won't drop HTTPS.

Deploying
---------

[](#deploying)

```
php artisan deploy production
```

This performs a zero-downtime deploy:

```
/home/deployer/app/
├── current -> releases/20260627T120000   # atomic symlink
├── releases/
│   ├── 20260627T120000/
│   └── …                                  # previous releases kept for rollback
└── shared/
    ├── .env                               # persists across deploys
    └── storage/                           # persists across deploys

```

The sequence:

1. Clone the repo into a fresh, timestamped release and record the commit in a `REVISION` file.
2. Symlink shared files/dirs (`.env`, `storage`) into the release, and **validate `APP_KEY`** — a missing or malformed key fails the deploy immediately instead of surfacing as a runtime 500.
3. `composer install --no-dev`, then (unless `--no-build`) build front-end assets with the **package manager the repo's lockfile calls for** — npm, yarn, pnpm or bun.
4. `storage:link`, cache config/routes/views/events, and run `php artisan migrate --force`.
5. **Atomically** swap the `current` symlink to the new release.
6. Reload PHP-FPM (to refresh OPcache) and restart queue workers.
7. Prune old releases beyond `keep_releases`.
8. **Probe the app** through Nginx from the server itself and report the HTTP status — a non-2xx/3xx answer prints a warning with the `rollback` command ready to paste (the deploy itself still succeeds; you judge).

Because the symlink is swapped only after the release is fully built and migrated, no request ever hits a half-deployed app.

The build step auto-detects the package manager from the committed lockfile — `package-lock.json` → npm, `yarn.lock` → yarn, `pnpm-lock.yaml` → pnpm, `bun.lockb`/`bun.lock` → bun. **yarn and pnpm work out of the box** (provisioning enables Corepack, which ships with Node); **bun** must be installed on the server if you use it (e.g. via a deploy `before` hook or manually).

Deploys (and rollbacks) take a **per-app lock** on the server, so two runs can't race each other — a concurrent attempt fails immediately, reporting when the other run started. If a deploy crashed and left a stale lock, re-run with `--force` to break it (`deploy --force` / `rollback --force`). A deploy that fails partway also **removes its half-built release**, so crashed deploys never leak disk (the active release is never touched).

### First deploy

[](#first-deploy)

On the very first deploy there's no `.env` yet, so the package seeds `shared/.env` from the first of these that exists in your repo: **`.env.`** (the environment is the server name, e.g. `.env.production`), then `.env`, then `.env.example`. Commit a `.env.production` (in your **private** repo) with the real `APP_KEY`, DB creds, etc. and the app is configured from the first deploy — otherwise bosun seeds a template, skips migrations and `queue:restart` (both need a configured, migrated app), and reminds you to fill it in on the server:

```
ssh deployer@203.0.113.10
nano /home/deployer/app/shared/.env   # set APP_KEY, DB creds, etc.
exit

php artisan deploy production          # this run will migrate
```

### Rolling back

[](#rolling-back)

```
php artisan rollback production                        # back to the previous release
php artisan rollback production --release=20260702T090000   # back to a specific release
```

The `current` symlink is swapped atomically to the earlier release and PHP-FPM/queue workers are reloaded, so it takes effect instantly. **Database migrations are not reversed** — if the bad deploy migrated the schema, check the older code is still compatible before rolling back.

### Checking status

[](#checking-status)

```
php artisan status production
```

```
  Release ......................... 20260702T090000 (9f8e7d6c5b4a)
  Releases kept ................................................ 3
  shared/.env ............................................ present
  nginx ................................................... active
  php8.3-fpm .............................................. active
  redis-server ............................................ active
  supervisor .............................................. active
  fail2ban ................................................ active
  Queue workers ....................................... 1/1 running
  Disk used ............................................ 24% (31G free)

```

It also warns when a deploy lock is currently held (with when that deploy started).

### Tailing logs

[](#tailing-logs)

```
php artisan logs production                       # last 100 lines of the Laravel log
php artisan logs production --type=nginx          # app / nginx / worker / fpm
php artisan logs production --lines=500
php artisan logs production --follow              # stream live (Ctrl-C to stop)
```

Connects as `root` by default so every log is readable (Nginx and PHP-FPM logs aren't group-readable by the deploy user); override with `--user`.

### Opening a shell

[](#opening-a-shell)

```
php artisan ssh production              # shell as the deploy user, in the current release
php artisan ssh production --user=root
```

This drops you into a login shell in `/current`. Unlike every other command — which talk to the server over phpseclib — `ssh` shells out to your **local `ssh` binary** for a real interactive terminal, so it needs `ssh` on your `PATH` (every dev machine has it).

### Useful options

[](#useful-options)

```
php artisan deploy staging                 # deploy to a different server
php artisan deploy production --branch=hotfix
php artisan deploy production --no-build    # skip front-end asset build
php artisan deploy production -v            # stream live command output
```

`-v` also works on `setup`, streaming the raw server output for each step.

Queue workers &amp; Horizon
---------------------------

[](#queue-workers--horizon)

Provisioning writes a Supervisor program for your queue, shaped by the `queue` config block:

```
'queue' => [
    'horizon' => false,        // run `php artisan horizon` instead of queue:work
    'connection' => 'redis',   // queue connection (empty = default)
    'queue' => 'high,default', // queue names (empty = default)
    'processes' => 2,          // worker count (ignored under Horizon)
    'tries' => 3,
    'timeout' => 60,           // Supervisor's stop grace scales with this
    'memory' => 128,
],
```

With `horizon => true` (or `DEPLOY_HORIZON=true`), Supervisor runs a single Horizon master — which manages its own worker pool from your app's `config/horizon.php` — and deploys/rollbacks cycle it with `horizon:terminate` instead of `queue:restart`. Re-run `setup` after changing any of these to rewrite the Supervisor program.

Deploy notifications
--------------------

[](#deploy-notifications)

Announce each deploy's result to Slack and/or a generic webhook:

```
'notifications' => [
    'slack' => env('DEPLOY_SLACK_WEBHOOK'),   // Slack incoming webhook — posts a formatted message
    'webhook' => env('DEPLOY_WEBHOOK'),        // any URL — receives a JSON payload
],
```

The webhook payload is `{ "event": "deploy", "status": "success|failure", "application", "server", "branch" }`. Both are opt-in (empty = off) and best-effort — a slow or down endpoint never affects the deploy itself. Notifications are sent from the machine running `deploy`, so it needs outbound network (fine for local and CI).

Deployment hooks
----------------

[](#deployment-hooks)

Run extra commands on the server during a deploy via `config/bosun.php`:

```
'hooks' => [
    'before' => ['php artisan down'],  // runs in the deploy path, before building
    'after'  => ['php artisan up'],    // runs in the new release
],
```

How it fits together
--------------------

[](#how-it-fits-together)

```
SetupCommand ─┐                          ┌─ Provisioner ──┐
              ├─ RemoteCommand (rendering)┤                ├─ RemoteScript ─ Connection (SSH)
DeployCommand ┘                          └─ DeploymentRunner ┘

```

- **`Connection`** is a tiny interface (`run`, `put`, `disconnect`). The real one uses phpseclib; tests use an in-memory fake.
- **`RemoteScript`** orchestrates a sequence of tasks but knows nothing about the console.
- **`RemoteCommand`** renders those tasks (compact ticks, or streamed output with `-v`) and reports failures.

That separation is why the provisioning and deployment logic is fully unit-tested without ever opening a socket.

Testing
-------

[](#testing)

```
composer install
composer test
```

Security notes
--------------

[](#security-notes)

- The deploy user is unprivileged. Its only `sudo` rights are passwordless reloads of PHP-FPM/Nginx and Supervisor control, written to `/etc/sudoers.d` and validated with `visudo -c`.
- **SSH password authentication is disabled** — key-only login. (So provision with a key; a password-only server can't be deployed to.)
- **fail2ban** is installed to throttle SSH brute-force attempts, and **automatic security updates** (`unattended-upgrades`) are enabled.
- Provisioning enables UFW and opens only SSH and HTTP/HTTPS. Nginx hides its version (`server_tokens off`), and with a domain set a catch-all server rejects requests for any other host.
- For HTTPS, point DNS at the server and run `php artisan secure ` (see [HTTPS](#https)).

License
-------

[](#license)

MIT — see [LICENSE.md](LICENSE.md).

###  Health Score

20

—

LowBetter than 12% of packages

Maintenance60

Regular maintenance activity

Popularity4

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/511ca83b905e928d191d304008fa0539d32c4b711eed9076fd80089481756f84?d=identicon)[cuonggt](/maintainers/cuonggt)

---

Top Contributors

[![cuonggt](https://avatars.githubusercontent.com/u/8156596?v=4)](https://github.com/cuonggt "cuonggt (45 commits)")

### Embed Badge

![Health badge](/badges/cuonggt-bosun/health.svg)

```
[![Health](https://phpackages.com/badges/cuonggt-bosun/health.svg)](https://phpackages.com/packages/cuonggt-bosun)
```

###  Alternatives

[pomander/pomander

Deployment for PHP

19615.7k2](/packages/pomander-pomander)[enumag/no-thanks

Prevents symfony/flex from printing thanks reminder.

3415.8k](/packages/enumag-no-thanks)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
