[5.2.2] — 2026-09-09
Two opt-in additions: Meilisearch for Laravel Scout, and an allowlisted cipi package for the host binaries a project sometimes needs. Neither is in the default stack, neither is installed by setup.sh or cipi self-update, and nothing on an existing server changes until someone asks for it.
Added — cipi package
cipi package list | install <name> | remove <name>. Installs host tools a Laravel project may need — image optimisers, ffmpeg, the ImageMagick CLI,pdftotext— from Ubuntu's own repositories. The allowlist is the feature: without it the command is a root apt shell with extra steps. The catalog is closed and every entry has to earn its place — a stateless binary from an Ubuntu repo (no daemon, no port, no credentials, no state outliving the process) with a real Laravel package behind it. Anything failing that is not a package but a service, and belongs tocipi search,cipi db installor the container branch. This is the same rule that put Meilisearch on the other side of the line, written down once.image-optimizers→jpegoptim optipng pngquant gifsicle webp, exactly the setspatie/laravel-image-optimizerdocuments.ffmpeg→pbmedia/laravel-ffmpeg. The install prints the warning that matters: run it from a queue worker, never from a web request — the FPM pool isrequest_terminate_timeout = 300and one ffmpeg will take every core on a box shared with MariaDB.imagemagick→ theconvert/magickCLI. This is genuinely missing today:php8.5-imagickdepends onlibmagickcore/libmagickwandand, through them, onimagemagick-6-common(config files only). The binaries live inimagemagick-6.q16, which nothing in the chain pulls — so PHP-side Imagick works whileexec('convert …')does not.poppler-utils→pdftotext, forspatie/pdf-to-textand for feeding PDF content to the Scout indexes above.- A single package inside a group is accepted as a name of its own, so
cipi package install webpworks alongsideinstall image-optimizers.
installshows what apt intends to do before doing it — package count and disk delta, read fromapt-get install -son the machine rather than from a number hardcoded here — then asks. Afterwards it verifies each expected binary is onPATHand names any that is not, instead of letting the application discover it.removepurges only the packages actually present (naming an absent one turns a no-op into an apt failure), then previews the orphaned dependenciesautoremovewould take and asks before running it — server-wide autoremove on a box that also runs MariaDB and PHP is not something to do silently.- Two notification triggers under a new Packages category:
package_install,package_remove. The panel sudoers file allowspackage listonly: installing packages as root stays with the operator on the CLI.
Notes — cipi package
- Chromium is deliberately not in the allowlist, and the refusal says why. On Ubuntu 24.04 there is no
chromiumdeb at all, andchromium-browseris a 48 kB transitional package whose dependencies aredebconfandsnapd— "Daemon and tooling that enable snap packages". Installing it would add a daemon and a snap that updates itself outside apt's control, which is precisely what this command exists not to do. Forspatie/browsershot, use Puppeteer's own Chromium (Node 20 is already installed) or Google's apt repository — both deliberate choices, not a side effect of an allowlist entry. - Ghostscript and
fonts-dejavu-coreare already installed and are not in the list: they arrive as Recommends ofphp-imagick, andsetup.shpasses no--no-install-recommends. - PDF through ImageMagick will still fail after installing it, and not for a missing package:
imagemagick-6-commonships/etc/ImageMagick-6/policy.xmlwith the PDF/PS/EPS coders disabled (the Ghostscript CVEs). That file exists on every Cipi server.pdftotextis not affected by the policy, which is part of whypoppler-utilsearned a place.
Added — cipi search
cipi search— a self-hosted search engine for Scout, wired the way the rest of Cipi is: native, not a container. Meilisearch is a single static Rust binary with no runtime dependencies, so it installs as a binary, a config file and a systemd unit alongside Nginx/PHP-FPM/MariaDB/Valkey. Engines that need a runtime around them (Elasticsearch, Typesense, Qdrant) belong in the container branch; this one does not, and that is the line. For a Laravel app it iscipi search enable <app>plus ascout:import— no Algolia account, no per-record billing.cipi search install [--version=] [--port=] [--force]. Downloads the release binary for this architecture from GitHub (amd64/aarch64), creates themeilisearchsystem user,/var/lib/meilisearch(0750),/etc/meilisearch.tomland a hardened unit, then waits onGET /healthbefore claiming success. Listens on 127.0.0.1 only — never a public address, so there is no firewall hole to open and no TLS to terminate. Meilisearch publishes no checksum beside its binaries, so the download is verified the only way available: it is executed and asked for its version before it is allowed to replace anything.cipi search enable <app>. Mints an API key scoped to the index pattern<app>-*with exactly the actions Scout calls (search,documents.*,indexes.*,settings.get,settings.update,tasks.get,stats.get— notkeys.*), then writesSCOUT_DRIVER,SCOUT_PREFIX=<app>-,MEILISEARCH_HOSTandMEILISEARCH_KEYintoshared/.env. The previousSCOUT_DRIVERis recorded sodisablecan put it back.cipi search disable <app> [--purge-indexes],list,status [--check] [--json],key show <app>.cipi search key rotate <app>andcipi search key rotate --master.cipi search upgrade [--version=] [--reset-data] [--yes]andcipi search remove [--keep-data].
- Isolation is the design, not a note in the docs. Meilisearch has no databases and no per-tenant separation, so one shared instance needs an explicit boundary. Cipi's is the index prefix, and Cipi writes it — the operator does not choose it: two apps that pick the same index name are not separated by a key pattern. It holds because a Cipi app username is
^[a-z][a-z0-9]{2,31}$with no hyphens, soblog-can never be a prefix ofblogs-or the reverse, and every index name belongs to exactly one app. An app that ignoresSCOUT_PREFIXdoes not collide with its neighbour — it gets a 403.enablestill asserts the non-overlap before minting a key. - The master key never reaches an app, an argument list, or a config file. It lives in the vault (
/etc/cipi/search.json) and in/etc/cipi/meilisearch.env, mode 0600 root:root, read by systemd as root before the unit drops to themeilisearchuser.--master-keyon anExecStartline would put it in/proc/<pid>/cmdline, which every app user on the box can read — the same reasoncipi search's own HTTP calls pass theAuthorizationheader to curl through a stdin config file instead of-H. cipi app deleterevokes the app's key and drops its indexes. The engine knows nothing aboutapps.json: without this, deleting an app left a live, working API key behind for a Unix user and a home that no longer exist.cipi app clonegives the clone its own key and prefix. The source'sMEILISEARCH_*andSCOUT_PREFIXare excluded from the copied.envfor a sharper reason than the Reverb exclusion next to it: the source's key is valid for the source's prefix, so a clone that inherited it would not fail — it would reindex straight into production's indexes.- Meilisearch appears in
cipi status,cipi service list|start|stop|restart(meilisearch, orsearch) and shell completion, but only where the unit exists. - Six notification triggers under a new Search category:
search_install,search_enable,search_disable,search_key_rotate,search_upgrade,search_remove. - The panel sudoers file allows
search status|list|enable|disable.install,upgrade,key rotateandremoveare deliberately not in it: they change or destroy server state and stay with the operator on the CLI.
Notes
- Rotating the master key regenerates every API key on the server. Meilisearch derives a key's value from its uid and the master key, so the uids survive and the values do not.
key rotate --mastertherefore rewrites every enabled app's.envin the same run — otherwise the whole server loses search at once, silently, at the next master key change. It says which apps it is about to touch before it starts. - Upgrades are the real cost of this feature, and the command is built around that. A Meilisearch store is readable only by the version that wrote it.
cipi search upgradeasks the new binary which in-place upgrade flag it supports (--upgrade-dbon current releases,--experimental-dumpless-upgradeon older ones) rather than guessing from a version number, applies it through a one-shot systemd drop-in, and then restarts without it to prove the unit still comes up on its own. If the engine refuses the store, the old binary is put back and the service restarted — nothing is dropped without an explicit answer. Only if you say so does it deletedata.ms, reissue every app key (a wiped store has no keys, so every.envwould otherwise be dead) and print thescout:importcommands. - Meilisearch is not in
cipi backupand that is deliberate. A Scout index is derived data;scout:importrebuilds it from the database that is backed up. Adding it to S3 would mean paying to store a rebuildable artifact whose format is pinned to an engine version. - RAM.
installrefuses below 512MB MemAvailable (--forceoverrides) and warns below 1GB. Meilisearch memory-maps its LMDB store, so its RSS understates what it asks the kernel for; on a 1GB VPS already running MariaDB tuned to that RAM, an OOM kill is a real outcome and the kernel does not always pick Meilisearch. - You may not need it. For a small dataset Scout's
databasedriver — or Postgres full-text, already in the stack since 5.0 — needs no extra service, no extra memory and no upgrade path.cipi search statussays so on a server where it is not installed.