All notable changes to Cipi are documented in this file.
- Panel GUI HTTP 500 after
cipi gui upgrade/ soft update — path repo forcipi/guidefaulted to a symlink into/opt/cipi/cipi-gui, outside PHP-FPMopen_basedir(/opt/cipi/gui/only). Restore Composeroptions.symlink=false(real copy invendor/), widenopen_basedirto include/opt/cipi/cipi-gui/, reinstall when a symlink is detected, and rewrite the FPM pool after upgrade/update. Migration 5.0.18 repairs existing panels. Immediate:cipi gui fix-permissions.
cipi self-updateappeared stuck on “Downloading cipi/gui…” until Enter — interactive TTY +timeout --foreground curl -scould stop on stdin (SIGTTIN). GUI tarball sync now uses curl’s own timeouts with stdin closed (</dev/null). Composer panel updates setCOMPOSER_ALLOW_SUPERUSER=1and also close stdin so root/--no-interactionruns never wait for a prompt.
cipi self-updatehung on GUI after API Packagist fix —cipi/guiis not on Packagist, so soft update still used a GitHub VCS repo and blocked onComposer VCS repo → https://github.com/cipi-sh/gui. Now syncs a timed GitHub tar.gz into/opt/cipi/cipi-guiand installs via path repo (composer require cipi/gui:@dev). Migration 5.0.16 unsets stalerepositories.cipi-gui.
cipi api update/ self-update API step hung whilecipi api upgradeworked — soft update forced a Composer VCS repo onhttps://github.com/cipi-sh/api(full git clone/scan). Upgrade installs from Packagist dist zips (composer require cipi/api), which already has 1.19.1. Soft update now matches upgrade: unset stalerepositories.cipi-api, thencomposer require cipi/apifrom Packagist (or local path). Migration 5.0.15 drops the VCS entry on existing panel apps.
cipi self-update/cipi api updatehung forever on panel API package update — baressh-keyscan github.com(no timeout) blocked before Composer, and VCScomposer updatehad no wall-clock limit (stderr was often discarded). Now: timed keyscan + official GitHub host-key fallback, Composer guard (timeout+COMPOSER_PROCESS_TIMEOUT), timed queue stop/migrate, andcipi api updateupdatescipi/apionly (full rebuild remainscipi api upgrade). Self-update delegates to_api_update_package/_gui_update_packageso the first upgrade pass already gets the fix.systemctl list-unit-files --quiet UNITis not an existence check — it exits 0 even when zero units match, so PostgreSQL (and optional PHP-FPM units) could be treated as present. Detection now usessystemctl catviasystemd_unit_existsfor DB engines andcipi service.cipi db engines— prints ASCIInot_installedinstead of an em dash, so API/GUI parsers cannot mis-read the status column.
cipi self-updateaborted with_cipi_composer_prepare_github: command not foundand left the panel on HTTP 500 — upgrading from older builds (e.g. v5.0.2) kept the pre-updateself-update/common.shhelpers in memory while sourcing the newgui.sh, which called a helper that did not exist yet. After the blanketchown -R root:root /opt/cipi, GUI.env/ storage stayed root-owned and PHP-FPM returned 500.lib/gui.shnow has the same inline fallback as the API (_gui_composer_prepare_github),ensure_cipi_gui_permissionsalso restores.envownership, andself-updatealways reclaims panel permissions at the end even if the Composer step fails. Migration 5.0.13 reclaims API/GUI ownership on update.
cipi php switchcould leave the panel on nginx 502 — it now rewrites the API and (when installed) GUI FPM pools onto the target PHP (listen = /run/php/cipi-api.sock/cipi-gui.sock), removes them from other versions, stops the old FPM to release the socks, recreates/run/php+ both sockets via the newphp*-fpm, waits until they exist, and rolls back both pools if either socket never appears.
cipi php switchfailed withunable to remove '/etc/alternatives/php.dpkg-tmp': Read-only file system— same remount-ro class as SMTP/basicauth/vault. Switch now remounts///etcwritable beforeupdate-alternatives, clears stale*.dpkg-tmpleftovers, retries once on RO errors, and prints an actionable recovery hint.
cipi php switchfalse failure after a successful CLI switch —update-alternatives --set phpoften printsusing /usr/bin/phpX.Y …then exits non-zero when a slave alternative (phar/phpdbg/…) is missing or broken. The panel API treated that as hard failure even though/usr/bin/phpalready pointed at the new version. Switch now verifies the resolved binary and continues when the master link is correct. Re-running switch when CLI is already on the target version still migrates a leftover API FPM pool.
- Panel API
PUT /api/php/defaultfailed withsudo: a terminal is required to read the password—/etc/sudoers.d/cipi-apiallowedphp list|install|removebut notphp switch, sowww-datacould not set the system default PHP without a TTY. Addedcipi php switch *to the whitelist. Migration 5.0.9 regenerates sudoers oncipi self-update.
- Panel API
cipi/apiupdates —cipi self-update/cipi api updatenow pullcipi/apifrom GitHub via Composer VCS when no bundled/opt/cipi/cipi-apicopy exists (same model ascipi/gui). Only runs if the panel API is already installed (/opt/cipi/api/artisan); it never installs the API on servers that do not use it. Fixes servers stuck on Packagist while GitHub already has 1.16+. - Non-interactive CLI flags —
parse_argsstrips wrapping quotes from--key='value'arguments built by the panel API (escapeshellarg), socipi smtp configure --host=…and similar commands receive clean values. - Migration 5.0.8 — reduced to a noop (the previous script duplicated self-update and could abort with
ensure_cipi_api_permissions: command not found). ssh_dir: unbound variableduring self-update —_cipi_composer_prepare_github/_api_composer_prepare_githubusedlocal ssh_dir=… kh="${ssh_dir}/…", which underset -uexpandsssh_dirbefore it is assigned and aborted the API/GUI composer step.
cipi smtp test/ send failed with/etc/msmtprc: Read-only file system— every send rewrites system/etc/msmtprc; when the kernel remounted/read-only that redirect aborted (same remount-ro class as basicauth/vault).lib/smtp.sh:_smtp_write_rcand_smtp_deletenow call_cipi_ensure_config_writable+ probe/etc(withmount -nremount) and print a clear recovery hint. Migration 5.0.7 remounts oncipi self-updateand regenerates/etc/msmtprcwhen SMTP is already configured.
cipi api ip-whitelist— restrict panel API / MCP clients by IP. Default file/etc/cipi/api-ip-whitelistis*(allow all). Subcommands:show(default),add,remove,set,allow-all. Entries: one IPv4/IPv6 or CIDR per line (or comma-separated--ips=).--jsonfor automation.- Non-interactive SMTP —
cipi smtp configure --host= --port= --user= --password= --from= --to= [--tls=on|off] [--enabled=on|off] [--no-test];cipi smtp status --json(password never printed);cipi smtp delete --force. - Healthcheck JSON —
cipi health list --json,cipi health check <app> --json(includes last state / failcount when present). cipi app webhook recreate <app> [--rotate-secret]— recreate the GitHub/GitLab deploy webhook; optional secret rotation updatesapps.json+shared/.envCIPI_WEBHOOK_TOKEN.--jsononcipi php list/cipi ssh list/cipi service list— structured output for the panel API.- Panel API sudoers —
php list|install|remove,ssh list|add|remove,service list|restart,status,db install|default,app webhook recreate,smtp status|configure|enable|disable|test|delete,api ip-whitelist(+ args). Migration 5.0.6 creates the default IP whitelist file and regenerates/etc/sudoers.d/cipi-apioncipi self-update.
cipi app edit --repository=recreated deploy key + webhook even when the URL was unchanged — provider cleanup/setup now runs only when the repository actually changes (same for unchanged--php/--branch).- GitHub/GitLab API
curlcould hang forever — added--connect-timeout 10 --max-time 30. cipi php removehung under the panel API — confirmation is skipped without a TTY (or with--force), matching other non-interactive commands.- Clearer error when editing to a PHP version that is not installed — suggests
cipi php install <ver>.
cipi basicauth enablestill failed after 5.0.4 with read-only/etc/nginx/cipi-basicauth— plainmount -o remount,rw /often fails while root is still RO (mtab updates / broken fstab UUID lookup).lib/vault.sh:_cipi_remount_rwusesmount -n+findmntSOURCE/TARGET, then re-creates/etc/cipi; basicauth also probes/remounts the nginx path and prints remount details on failure. Migration 5.0.5 applies the same recovery oncipi self-update.
cipi app runalways failed with/usr/bin/env: '--': No such file or directory— GNUenvtreats a bare--afterNAME=VALUEassignments as the command to execute (not an option terminator).app_runwas invoking/usr/bin/env -C … VAR=1 -- cmd, so every whitelisted command (including GUI “App commands”) exec’d--. Now:/usr/bin/env -C "$workdir" VAR=… cmd args….cipi basicauth enable/ vault writes on remount-ro root — when the kernel remounted/read-only,mkdir /etc/nginx/cipi-basicauthandvault_write apps.jsonfailed (Read-only file system), while_basicauth_set_userstill returned success because trailingchown/chmodused|| true.lib/vault.sh: mutating writes call_cipi_ensure_config_writable(probe → best-effortmount -o remount,rw /→ re-probe); read paths keep using_cipi_config_writableonly (no remount onsource/db list).lib/app.sh: basicauth credential writes fail closed with a clear error if the filesystem stays read-only. Migration 5.0.4 remounts oncipi self-updatewhen/etc/cipiis still RO.
- Non-interactive
.envmanagement —cipi app env <app>still opens nano by default; API/scripts can use--show/--get=KEY/--set=KEY=VALUE/--unset=KEYwith optional--json. - Non-interactive Composer
auth.json—cipi auth create|delete <app> [--force](skips confirm when no TTY);cipi auth edit <app> --file=PATH;cipi auth show <app> --json. cipi app run <app> <cmd> [args…]— whitelisted non-interactive binaries as the app user (composer,npm/npx/yarn/pnpm,ls/ll,cat/head/tail, filesystem helpers, archives,git,php,node,find). List withcipi app run --commands [--json]. No editors/pagers/shells/REPLs (nano,vim,less,bash,tinker, …); interactive flags blocked.- Structured deploy config —
cipi app deploy-config <app>show/edit durable recipe options inapps.json(regeneratesdeploy.php— safe alternative to free-form PHP):--keep-releases=N,--migrate/--no-migrate,--optimize/--no-optimize,--storage-link/--no-storage-link,--queue-restart/--no-queue-restart,--horizon-terminate/--no-horizon-terminate,--extra-artisan=cmd1,cmd2,--node-build/--predeploy-snapshot(and clear flags). - Panel API sudoers —
app env,app artisan,app run,auth create|edit|show|deletein/etc/sudoers.d/cipi-api. Migration 5.0.3 regenerates sudoers + Laravel/Octanedeploy.phponcipi self-update.
horizon:terminatereportsNamespaceNotFoundExceptionwhen Horizon is not installed — Deploy templates always ranphp artisan horizon:terminatebefore symlink. With|| truethe deploy still succeeded, but apps withoutlaravel/horizonthrew Symfony’s “no commands defined in the horizon namespace”, which exception digests (Flare/Sentry/etc.) reported. Templates now skip artisan unlessvendor/laravel/horizon/composer.jsonexists (same guard foroctane:reload+laravel/octane). Migration 5.0.2 regenerates existingdeploy.phpfiles.
- First deploy fails on
horizon:terminate/readlink …/current— Deployer resolves{{current_path}}withreadlinkbefore the shell|| truecan run, so a brand-new app (nocurrentsymlink yet) aborted atbefore('deploy:symlink', 'horizon:terminate'). Templates now guard with[ -L {{deploy_path}}/current ]and call artisan via{{deploy_path}}/current. Migration 5.0.1 regenerates existingdeploy.phpfiles.
- Optional Laravel Octane (FrankenPHP) — create Laravel apps that serve HTTP via Octane instead of PHP-FPM, in parallel with classic FPM apps on the same server:
cipi app create --octaneor--octane=frankenphp(Laravel only; rejected with--custom)- Allocates a localhost port (
8100–8999), storesoctane/octane_portinapps.json - Nginx vhost proxies to Octane (
proxy_pass) and serves static files fromcurrent/public— no per-app FPM pool - Supervisor program
${app}-octanealongside queue workers; Deployer templatelaravel-octane.phprestarts/reloads on deploy .env:OCTANE_SERVER=frankenphp,OCTANE_HTTPS=true- Requires
laravel/octane+php artisan octane:install --server=frankenphpin the app repo (Octane starts after the first successful deploy)
cipi app convert <app> --to=octane|fpm— convert an existing Laravel app between PHP-FPM and Octane (pool/vhost/supervisor/deployer/.env; SSL re-applied when present)- Laravel Reverb —
cipi app reverb enable|disable|status: localhost port9000–9099, Supervisor${app}-reverb, Nginx/appWebSocket proxy,.envREVERB_* - Laravel Horizon —
cipi worker horizon enable|disable|status: mutually exclusive withqueue:workworkers; deploy runshorizon:terminate+cipi-workerrestart - Scheduler CLI —
cipi schedule on|off|status <app>(crontabschedule:runalready existed; now manageable;schedulein apps.json) - Node build on deploy —
cipi app edit --node-build='npm ci && npm run build'/--no-node-build; Deployer runs.deployer/node-build.shafter vendors (fail-closed; validated command) cipi app clone <src> --domain=…— staging as a new app ([--name=] [--branch=] [--with-db|--no-db]); setscloned_from; does not copy webhook/git IDs- Pre-deploy DB snapshot — opt-in via
predeploy_snapshot/cipi deploy --snapshot/--snapshot-required; dump under/var/log/cipi/backups/; code rollback does not auto-restore DB - Resource limits —
cipi app limits <app>(--fpm-max-children,--memory-limit,--octane-workers,--worker-procs) with hard caps - SSL DNS-01 (Cloudflare) —
cipi ssl dns set --provider=cloudflare --token=thencipi ssl install <app> --dns=cloudflare [--wildcard]; HTTP-01 remains default - HTTP healthchecks —
cipi health set|unset|check|list; cron every 5 minutes; notifyhealth_failafter 3 consecutive failures apps-public.json— also exposesreverb,reverb_port,horizon,schedule,node_build,cloned_from,predeploy_snapshot,limits,health_url,health_expect,ssl_dns_provider
cipi self-updatestuck on Migration 4.7.16 — the historical migration used|as theseddelimiter while the replacement contained|| true, sosedaborted withunknown option to sand left the server at the previous version (files already copied). 4.7.16 now no-ops whencommon.shhas no bare initchmod, and uses#delimiters when patching. Unblocks upgrades from versions below 4.7.16 (e.g. 4.7.14 → 4.8.x).
cipi www— manage www/apex aliases and canonical redirects:cipi www add <app>— add the counterpart host (www.+ apex, or apex when primary is alreadywww.*)cipi www force-to-root <app>— 301 redirectwww.domain→domain(auto-adds the missing alias)cipi www force-from-root <app>— 301 redirectdomain→www.domaincipi www clear <app>/cipi www status <app>— clear or inspect redirect state- State lives in
apps.json(www_redirect);_create_nginx_vhostemits a dedicated redirect server block (ACME path kept public). Survives vhost regeneration; SSL is re-applied viacertbot install --redirect.
cipi ssl force <app>— re-apply HTTP → HTTPS redirect for an app that already has a Let's Encrypt cert (no new issuance). Also set automatically bycipi ssl install(force_httpsinapps.json).- Multi-engine databases (MariaDB + PostgreSQL) — MariaDB stays native on 3306; optional PostgreSQL on 5432:
cipi db install pgsql/cipi db uninstall pgsql|mariadb— install or remove a non-default engine (data destroyed on uninstall)cipi db default mariadb|pgsql— server-wide default used when--engineis omittedcipi db engines— show installed engines, ports, and defaultcipi db create|list|delete|backup|restore|passwordaccept--engine=mariadb|pgsqlcipi app create --engine=pgsql(interactive prompt when creating Laravel apps);.env+ connection URL match the engine- App metadata stores
engine; backup/sync/reset-db-password follow it cipi reset db-password [--engine=]resets the root password for the chosen (or default) enginecipi servicerecognizespostgresql(aliases:pgsql,postgres)
cipi alias add|remove— after regenerating the vhost, re-apply SSL withcertbot install --redirect(same pattern as basicauth/suspend) so HTTPS is not dropped.apps-public.json— projection now includeswww_redirect,force_https, andengine. Migration 4.8.0 regenerates it, nests legacydatabases.jsonundermariadb, backfillsengine=mariadbon apps, and refreshes the panel API sudoers whitelist.
cipi help— main help is now a short index grouped by area (Core, Apps & deploy, Security, Panel, Ops). Full command lists live in topic help:cipi help <topic>(e.g.app,deploy,gui) orcipi <command> help. Usecipi help allfor the previous monolithic dump.
cipi smtpPermission denied / AppArmor — msmtp cannot use/etc/cipi/.msmtprc(setgid + AppArmor deny read on/etc/cipi/*), cannot runpasswordevalhelpers likecat, and cannot write custom log paths under/var/log. Send now uses only system/etc/msmtprc(root:msmtp640) with an embedded password — no-C, nopasswordeval, nologfile. Legacy/etc/cipi/.msmtprcis removed. Configure/test show real msmtp errors and check host reachability. Migration 4.7.22.
cipi smtp→msmtp: /etc/cipi/.msmtprc: Permission denied— Debian/Ubuntu shipmsmtpsetgid (msmtpgroup). After setgid the process can no longer traverse/etc/cipi(750root:cipi-api) or read vault secrets viapasswordeval. Cipi now clears the setgid bit on the msmtp binary (mail is only sent as root). Migration 4.7.21 applies this on existing servers and regenerates.msmtprc.
cipi smtp configure/cipi smtp testfailed with working credentials — several msmtp integration bugs:- Password was written via an unquoted heredoc, so
$/ backticks in passwords were shell-expanded and corrupted before msmtp saw them. .msmtprcstored the password in cleartext; msmtp then refused the file when permissions were not strictly0600(common after group changes under/etc/cipi).- Hardcoded
tls_trust_file /etc/ssl/certs/ca-certificates.crtmade send fail when that path was missing; trust file is now used only if present. tls_starttlsstayedoneven when TLS was disabled.- Test failures hid the real msmtp error (
2>/dev/null) and did not detect an unreachable host/port.
- Password was written via an unquoted heredoc, so
- SMTP now uses
passwordeval(lib/cipi-smtp-pass.sh) so the password stays in encryptedsmtp.jsononly; configure/test print the msmtp error and check TCP reachability. Migration 4.7.20 regenerates.msmtprcon existing servers.
cipi deploypassword prompt /Permission denied (publickey,password)— new apps created under umask002got~/.sshas775(group-writable). OpenSSH StrictModes then refused pubkey auth withAuthentication refused: bad ownership or modes for directory /home/<app>/.ssh, so Deployer's localhost SSH fell through to a password prompt.cipi app createnow forceschmod 700 ~/.ssh; Migration 4.7.19 repairs existing apps oncipi self-update.cipi app deleteleft orphan Linux users / homes / SSH keys —userdel -r … || trueoften failed silently when cron or other processes still held the UID, so/home/<app>(and~/.sshdeploy keys) stayed on disk after the app was removed fromapps.json. Delete now kills leftover processes, falls back torm -rf /home/<app>, and runspurge_orphan_app_usersto sweep other Cipi-looking leftovers not inapps.json. Migration 4.7.19 /cipi self-updatealso purge orphans.
cipi db liston Ubuntu 25.10+ / 26.04 — Migration 4.7.18 completes what 4.7.16 / 4.7.17 left incomplete:- Read-only
/etc/cipi—lib/common.sh/lib/vault.sh:_cipi_config_writable, no initchmod,_cipi_safe_chmod; migration re-installs lib from self-update bundle (or GitHub) when needed. FixesHTTP 503: chmod … Read-only file systemfromcipi-cli db list. - sudo-rs sudoers + API
open_basedir— re-applied idempotently. mount -o remount,rw /when/etc/cipiis read-only; refreshapps-public.json;sudo cipi db listsmoke test.
- Read-only
cipi db listmissing databases —lib/db.shnow usesinformation_schema.schemataso empty databases (freshcipi db create, pre-migrate app DBs) show as 0 MB.cipi db listsilent MariaDB failures — surfaces vault/MariaDB errors instead of a blank table.
- Panel API on Ubuntu 25.10+ / 26.04 (consolidated) — Migration 4.7.17 applies in one
cipi self-updatepass:- sudo-rs sudoers — rewrites
/etc/sudoers.d/cipi-apivialib/cipi-api-sudoers.sh(cipi db restore *instead of* *). - Log viewer /
CipiLogReader— API PHP-FPMopen_basedirnow includes/usr/local/bin/sois_executable()oncipi-read-app-logsandcipino longer fatals beforesudofallback. - Read-only
/etc/cipi—common.sh/vault.shguards (shipped inlib/*.sh); migration verifies_cipi_config_writableis present.
- sudo-rs sudoers — rewrites
- Panel API /
sudo cipifailing on read-only/etc/cipi— sourcingcommon.shalways ranchmod 700 /etc/cipi, recreatedapps-public.json(vault_read apps.json+ write), and initialized missing vault files — all requiring writes under/etc/cipi. On a read-only root (kernelremount-ro, dual-boot NTFS left dirty, etc.) that aborted even read-only commands likecipi db list, with errors such aschmod: Read-only file system (os error 30),apps-public.json: Read-only file system, orvault: failed to decrypt apps.json. Added_cipi_config_writable; initchmod/mkdir,ensure_apps_json_api_access, andvault_initare now best-effort when/etc/cipicannot be written. Fix ships inlib/common.sh/lib/vault.shoncipi self-update(verified by migration 4.7.17).
- Panel API broken on Ubuntu 25.10+ (
sudo-rs) —/etc/sudoers.d/cipi-apiusedcipi db restore * *, which sudo-rs rejects (wildcards are not allowed in command arguments). The invalid rule made sudo ignore the whole file, sowww-datacould not run any whitelisted API command (I'm afraid I can't do that). Replaced withcipi db restore *(trailing*only, per sudo-rs). Centralized the whitelist inlib/cipi-api-sudoers.sh. Migration 4.7.15 rewrites sudoers on existing servers viacipi self-update.
- Backup fails on large apps when
/tmpis tmpfs —cipi backup runwrote gzipped DB dumps and file archives under/tmp, which on many servers is a small RAM-backed tmpfs. Staging now defaults to/var/tmp(disk). Override withtmpdirinbackup.json(set viacipi backup configure) orCIPI_BACKUP_TMPDIR. Fixes #500.
- Weekly PHP security patch check — PHP packages are excluded from
unattended-upgrades(managed by Cipi).cipi php upgraderunsapt-get updateand--only-upgradeon all installedphp*/libphp*packages, restarts affected PHP-FPM pools, and emails when upgrades were applied (php_upgradetrigger). Root crontab: Sunday 03:30 viacipi-cron-notify. Log:/var/log/cipi/php-upgrade.log. Migration 4.7.13 adds the cron on existing servers and runs the check immediately (2026-07 PHP 8.x security release).
- SQL injection in
cipi dbcommands —_db_delete,_db_backup,_db_restore, and_db_password(lib/db.sh) interpolated the<name>argument (and, for delete/password, the storeduserfield) directly intomariadb -estatements without validation, unlike_db_createwhich already validatedname. A crafted database name (e.g. containing a quote or backtick) could break out of the SQL string/identifier context and execute arbitrary statements as the MariaDB root user;_db_create --user=was also unvalidated, allowing the same injection at creation time. Addedvalidate_db_name()(lib/common.sh) and applied it toname/userin every_db_*function, including the vault-storeduserfallback used by delete/password. - PHP code injection via
cipi app create --repository=/--branch=andcipi app edit— these values were substituted unsanitized into the single-quoted PHP string literalsset('repository', '...')/set('branch', '...')when generating each app'sdeploy.php(lib/app.sh,lib/deployer/{laravel,custom}.php); the existingsedescaping only handled sed metacharacters, not the PHP string delimiter. A crafted branch/repository value (e.g.x'); system('...'); //) could break out of the string literal and inject arbitrary PHP — executed as the app's Linux user on every deploy (manual, cron, or webhook-triggered), reachable even via a REST API token scoped only toapps-edit. Addedvalidate_git_branch()andvalidate_git_repository()(lib/common.sh, restricted charset) and enforced them inapp_createandapp_editbefore the values are written anywhere.
No migration required — both fixes are input-validation only and take effect immediately after cipi self-update; no existing app/database configuration needs to be regenerated.
- Setup post-install guide — after the credentials summary,
setup.shnow prints a short step-by-step guide for the optional Panel API (cipi api <domain>→cipi api ssl→cipi api token create) and Web GUI (cipi gui <domain>→cipi gui ssl), including DNS hints and what each layer does.
cipi app logs read <app>— paginated log snapshot for the panel API (GET /api/apps/{name}/logs) with--type=,--page=,--per-page=; emits===CIPI_LOG_FILE:…===markers. Wired ascipi app logs readsubcommand (distinct from interactivecipi app logstail).
- GUI log viewer still empty for Laravel logs on servers that already ran migration 4.7.8 —
open_basediron the API PHP pool blocks reads under/home/*, so log content must be fetched viasudo cipi app logs read. Migration 4.7.10 ensures/etc/sudoers.d/cipi-apiwhitelists that command (andcipi-read-app-logs) on existing servers.
- Panel API still could not read Laravel log files after 4.7.8 — traverse ACLs on
shared/storage/logswere not enough when log files areapp:app664.ensure_app_logs_permissionsnow grantsu:cipi:ron each*.logfile (and clears stale ACLs first). Migration 4.7.9 retrofits all known apps oncipi self-update.
/usr/local/bin/cipi-read-app-logs— root helper for paginated tail/head of app log globs under/home/*/logs/and/home/*/shared/storage/logs/(path-validated; used by the panel API via sudo).
- GUI log viewer could not read files under
/home/*from the API — first sudoers pass forwww-data:cipi-read-app-logs *plus suspend/basicauth entries from 4.7.6. Migration 4.7.8 installs the helper and rewrites/etc/sudoers.d/cipi-apion existing servers.
- GUI apps list always showed “Suspend” / wrong Basic Auth state —
apps-public.json(API read model forGET /apps) omittedsuspendedandbasic_authfrom the projection even though they live inapps.json._update_apps_publicnow includes both flags. Migration 4.7.7 regeneratesapps-public.jsononcipi self-update.
- Panel Basic Auth / Suspend failing with
sudo: a terminal is required to read the password— the panel runs aswww-dataand callssudo cipi basicauth enable|disable|status(abilityapps-basicauth) andsudo cipi app suspend|unsuspend(abilityapps-suspend), but these were missing from the/etc/sudoers.d/cipi-apiwhitelist, sosudoprompted for a password and failed without a TTY. Added them to the whitelist. Migration 4.7.6 applies the same fix on existing servers viacipi self-update. cipi basicauth disablerefusing withBasic auth is not enabledwhile auth was still live — whenbasic_authinapps.jsongot reset without regenerating the vhost (Nginx kept enforcing the oldauth_basicblock and the htpasswd file lingered), disable relied on the flag alone and left the app stuck protected. It now also disables when the htpasswd file exists, so it always cleans up the file and regenerates the vhost.- Panel DB delete / restore and deploy rollback hanging on a
[y/N]prompt —cipi db delete,cipi db restoreandcipi deploy --rollbackalways calledconfirm, whose blockingreadnever returns under the API/UI job runner (no TTY), so the job hung forever showingDelete database 'x'? [y/N]:. They now skip confirmation when--forceis passed or stdin is not a terminal (matchingcipi app delete --force).
- GUI package source —
cipi guinow installs and updatescipi/guifrom GitHub via Composer VCS (dev-main). Thecipi-gui/directory was removed from this repo (GUI lives in its own repository). PHP-FPMopen_basediris simplified to/opt/cipi/gui/only. Migration 4.7.5 migrates existing servers and removes the legacy/opt/cipi/cipi-guibundle. cipi gui update— now runscomposer update cipi/gui,migrate --force, andcipi:gui-refresh-theme(when available). New subcommandcipi gui refresh-themefor theme-only reloads.
cipi gui remove— uninstall the web control panel when configured (Nginx, FPM, cron, SSL, Laravel app, vault config). Aliasuninstall;--forceskips confirmation.
cipi gui reset-user— fixes login failing after reset (These credentials do not match our records) caused by double password hashing (Laravel 12hashedcast +Hash::make()incipi/gui's--reset). Always resets vialib/gui-reset-admin.php(plain password + session purge). Payload written understorage/app/aswww-data.cipi gui/gui upgradeinstall — fixesgetcwd: cannot access parent directorieswhen the shell cwd was inside/tmp/cipi-gui-buildor/opt/cipi/guiduringrm -rf. Build dir moved to/var/tmp/cipi-gui-build.*with_gui_cd_safebefore destructive ops;open_basedirincludes/var/tmp/.
- GUI
open_basedirstill blocking/opt/cipi/cipi-gui/on existing servers —cipi gui fix-permissionsnow runs a full_gui_repair_runtime: copiescipi/guiintovendor/(symlink: false+composer reinstall), rewrites the FPM pool with/opt/cipi/cipi-gui/inopen_basedir, and clears caches. Migration 4.7.3 applies the same repair oncipi self-update. - GUI app detail 500 (
Undefined variable $name) —AppDetail::mount()now accepts the{name}route parameter (mount(string $name)).
- GUI giant icons / broken layout — the inline CSS subset was missing size utilities used in Blade (
h-9,h-12,h-16,mx-auto,text-surface-600,bg-surface-900/50, …); SVGs without matching rules rendered at full browser default size. Added the missing utilities plus safe SVG defaults incipi/guipartials/styles.blade.php. - GUI Livewire / Alpine JS conflicts —
layouts/app.blade.phploaded Alpine from jsDelivr on top of Livewire 3’s bundled Alpine, breakingwire:click, polling, andx-datatoasts. Removed the duplicate script tag. - GUI HTTPS / Livewire — Nginx vhost now passes
HTTP_X_FORWARDED_PROTOandHTTPSto PHP-FPM so Laravel generates correct URLs behind TLS. Migration 4.7.2 patches existing vhosts in place (preserves certbot SSL blocks) and updatescipi/gui.
- GUI HTTP 500 (
open_basedir/CipiGuiServiceProvider) — thecipi-guiPHP-FPM pool allowed only/opt/cipi/gui/, but Composer's path repository symlinkedcipi/guifrom/opt/cipi/cipi-gui/, so autoload failed at runtime.lib/gui.shnow setsopen_basedirto include/opt/cipi/cipi-gui/, configures the path repo withsymlink: false(package copied intovendor/), and refreshes the FPM pool oncipi gui update/fix-permissions. Migration 4.7.1 retrofits existing servers.
cipi gui— optional web control panel for managing one or more Cipi servers via the REST API (cipi/gui).cipi gui <domain>provisions a Laravel host at/opt/cipi/gui(dedicated PHP-FPM pool, Nginx vhost, scheduler cron), prompts interactively for admin email and password (password policy: min 12 chars, upper + lower + digit + special, max 4 identical chars in a row), and stores config in/etc/cipi/gui.json. Subcommands:ssl,update(softcomposer update),upgrade(full rebuild),status,fix-permissions,reset-user(rewrite admin email/name/password and clear 2FA; same password policy; aliasreset-password). Session login with optional TOTP 2FA; no local queue worker — remote async jobs are polled via the managed servers'cipi api. Thecipi/guiComposer package is bundled at/opt/cipi/cipi-gui(same model ascipi-api). Requirescipi apienabled on each managed server with a token covering the full ability set.
setup.shfailed on fresh VPS whenunattended-upgradesholds the apt lock — the typical flow (new VPS → paste the install command) often collides with the first automatic security update, so the very firstapt-getcould exit immediately with Could not get lock and abort the install.setup.shnow waits up to 300s for the lock on everyapt-getviaDPkg::Lock::Timeout, shows a short System updates in progress… message when the lock is already held, and writes/etc/apt/apt.conf.d/00cipi-lock-timeoutso later steps (PPA, NodeSource, cron) inherit the same timeout.set -o pipefailwas added so piped installers (e.g. NodeSource) cannot fail silently.lib/php-apt.sh: directdpkg -i(Sury keyring) usescipi_wait_for_dpkg_lockbecause dpkg does not honour the apt timeout; removed|| trueon that path so a broken keyring install stops instead of continuing with missing PHP packages. Migration 4.6.7 applies the apt.conf snippet on existing servers.
setup.shMariaDB / PHP on Ubuntu 26.04 (resolute) — the installer still always added the MariaDB.org 11.4 repo andppa:ondrej/php, which have noresolutesuite yet (Release file not found at Installing MariaDB…).setup.shnow bootstrapslib/php-apt.sh(curl from GitHub when/opt/cipidoes not exist), sanitises broken third-party sources left by a partial install, usesmariadb_setup_apt_repo(MariaDB.org when available, otherwise Ubuntu main — 11.8.x on 26.04), andphp_setup_apt_sources(ondrej → packages.sury.org → archive).mariadb_setup_apt_repo/mariadb_apt_source_labeladded tolib/php-apt.sh.
- Self-update stuck on 4.6.2–4.6.4 (readonly / unset
CIPI_*) —lib/api.shassignedCIPI_API_ROOT/CIPI_API_CONFIGasreadonlywithout guards, so sourcingapi.shinside migrations could abort the loop; guards now matchcommon.sh.self-updateexportedCIPI_LIB=…after the main binary had already markedCIPI_LIB/CIPI_CONFIG/CIPI_LOGreadonly (CIPI_LIB: readonly variableat line 64) — it now usesexport CIPI_LIB CIPI_CONFIG CIPI_LOG(by name) plusCIPI_API_ROOTdefault.cipi-cron-notify,cipi-auth-notify, andcipi-app-notifyuse the same guard pattern. Migration 4.6.5 idempotently repairs hybridlib/*.shon disk (skips plainCIPI_*=heredoc lines inapp.sh; pure bash, mawk-safe).
common.shCIPI_LIB when sourced outsidecipi— Migrations and other callers thatsource common.shwithout going through the main binary leftCIPI_LIBunset, sosource "${CIPI_LIB}/vault.sh"failed mid–self-update (notably migration 4.6.3 after token-abilities / api.sh steps).common.shnow derivesCIPI_LIBfrom its own path viaBASH_SOURCEwhen unset (same pattern asCIPI_CONFIG/CIPI_LOG), with no hardcoded/opt/cipi/lib.- Migration runner hygiene —
self-updateexportsCIPI_LIB,CIPI_CONFIG, andCIPI_LOGbefore running migrations, runs each migration underset -euo pipefail, and prints a clear migration failed — version not updated message instead of a raw bash traceback. - Partial 4.6.3 recovery — Servers stuck at 4.6.2 with libs already copied from 4.6.3 can re-run
cipi self-update: migration 4.6.3 remains idempotent; 4.6.4 verifies the path fix and completes any remaining cron / notifications steps. Emergency pre-release patch:lib/fix-common-readonly.shnow also applies theCIPI_LIBderive block.
cipi notifications— granular email trigger control when SMTP is configured.cipi notifications listshows every event grouped by category (apps, deploy, SSL, aliases, PHP, security, cron, …) with on/off status;enable/disable <trigger>,enable-all,disable-all, andresettoggle what gets emailed. All triggers are on by default; events are always logged to/var/log/cipi/events.logregardless. Config:/etc/cipi/notifications.json.cipi_notify()and the PAM / cron / webhook helpers (cipi-auth-notify,cipi-cron-notify,cipi-app-notify) respect the trigger map. New notifications were added for actions that previously only logged (aliases, SSL, deploy success/rollback, PHP install/remove, DB create/delete, workers, API, git, sync, services). Migration 4.6.3 seedsnotifications.jsonon existing servers.
cipi api token createability list — readstoken-abilities.txtfrom the panel API package (includesstatus-view,apps-suspend,apps-basicauth, and all other REST abilities).lib/migrations/4.6.3.shretrofits existing servers: writes/opt/cipi/api/token-abilities.txtand patcheslib/api.shwhen it still has the legacy 14-line hardcoded list.- Nightly panel API update —
/usr/local/bin/cipi-api-updaterunscipi api update(softcomposer update+ migrations) every night at 04:30 via/etc/cron.d/cipi-api, wrapped withcipi-cron-notifyfor failure alerts. Log:/var/log/cipi-api-update.log.
- Change primary domain via
cipi app edit --domain=— you can now rename an app's primary domain without recreating it:cipi app edit <app> --domain=<new>validates the new name (format + uniqueness), promotes it to primary, moves the previous primary into aliases (so the old URL keeps working), regenerates the Nginx vhost, updatesAPP_URLinshared/.envwhen present, refreshes the GitHub/GitLab webhook URL when git integration is auto-configured (deploy key unchanged), and re-issues Let's Encrypt when a certificate already existed for the old primary (otherwise it reminds you to runcipi ssl installonce DNS for the new domain is ready). Promoting an existing alias to primary works too (e.g. swapapp.example.com↔www.example.com). Composable with the existing--php,--branch, and--repositoryflags.
- Composer upgraded to 2.10.1+ — servers provisioned earlier kept whatever Composer build the
getcomposer.orginstaller shipped at the time (some as old as 2.9.x), which lags the current stable 2.x release.setup.shnow enforces a Composer 2.10.1 floor on fresh installs: after running the installer it checks the reported version and, when below the floor, runscomposer self-update --2(falling back to a pinnedself-update 2.10.1). Migration 4.6.1 applies the same guard to existing servers oncipi self-update— it self-updates the system Composer (/usr/local/bin/composer) to the latest stable 2.x when below 2.10.1, and is idempotent (servers already at/above the floor are skipped).
- Supported Ubuntu releases — fresh installs via
setup.shnow accept Ubuntu 24.04 or 26.04 only (no other version, including interim releases). The initial requirements check uses an exactVERSION_IDmatch instead of a24.04+floor.
- PHP install /
apt-get updatefailed on Ubuntu 26.04 (resolute) —setup.shandcipi php installalways addedppa:ondrej/php, but Launchpad does not publish aresolutesuite yet, soapt-get updatefailed with Release file not found (including on re-runs at Installing base packages when a broken PPA source was left from a partial install). Cipi now probes PHP APT sources per codename: when Launchpad ondrej has a suite (noble, jammy, …) it uses the PPA as before; when it does not (resolute / Ubuntu 26.04) it configures packages.sury.org instead (same maintainer, co-installablephp8.3/php8.4/php8.5— multi-PHP works on 26.04); if neither repo is available, it falls back to Ubuntu main (single version).lib/php-apt.shcentralises the logic;setup.shbootstraps it before the firstapt-get updateand sanitises stale broken sources;cipi php installuses the same helper. Migration 4.5.12 removes a leftover broken ondrej source and wires up packages.sury.org where needed.
- MariaDB install failed on Ubuntu 26.04 (resolute) —
setup.shalways added the MariaDB.org 11.4 APT repo using$(lsb_release -cs), but that repository does not publish aresolutesuite yet, soapt-get updatefailed with Release file not found. The installer now probes the repo for a validReleasefile per codename: when the suite exists (e.g. noble, jammy) it uses MariaDB.org 11.4 as before; when it does not (e.g. resolute / Ubuntu 26.04) it skips the third-party repo and installs MariaDB from Ubuntu main (11.8.x on 26.04). A leftover broken/etc/apt/sources.list.d/mariadb.listfrom a failed run is removed automatically.
- Fresh install failed after nginx.org package install — the official nginx.org DEB does not create
/etc/nginx/sites-available/or/etc/nginx/sites-enabled/(Debian/Ubuntu convention only), sosetup.shaborted with No such file or directory when writing the default vhost.setup.shnow creates those directories (and/var/www/html) and removes the stock/etc/nginx/conf.d/default.confso it does not conflict with Cipi's default server block. Migration 4.5.10 applies the same layout fix on existing servers (idempotent).
- Nginx upgraded to mainline 1.29.8+ (HTTP/2 bomb mitigation) — a newly disclosed remote DoS dubbed HTTP/2 bomb chains HPACK header amplification with a zero-byte HTTP/2 flow-control window so the server never frees allocated memory; nginx fixed this in 1.29.8 with the
max_headersdirective (default 1000). Ubuntu 24.04's distro nginx (1.24.x) does not include that fix, so Cipi now installs and upgrades nginx from the nginx.org mainline APT repository (pinned with99nginxpreferences) instead of the Ubuntu archive. Fresh installs viasetup.shget mainline nginx out of the box; existing servers receive migration 4.5.9 oncipi self-update, which adds the repo, upgrades the package, and rewrites/etc/nginx/nginx.confwithmax_headers 1000. The Ubuntu-onlylibnginx-mod-http-headers-more-filtermodule (used formore_clear_headers) is removed — it is incompatible with nginx.org packages;server_tokens offand per-vhostfastcgi_hide_header X-Powered-Byremain in place. Nginx stays on the existing unattended-upgrades blacklist (Cipi-managed upgrades only).
- App suspend / unsuspend — take any app (Laravel or custom) offline without deleting it:
cipi app suspend <app>replaces the app's Nginx vhost with a generic static suspension page served as HTTP 503, andcipi app unsuspend <app>restores the normal vhost. State lives inapps.json(suspended), and_create_nginx_vhostrenders the suspended vhost whenever the flag is set, so suspension survives vhost regeneration (alias add/remove, PHP edit) andcertbot installclones it into the:443server block — HTTPS is suspended too. The ACME challenge path (/.well-known/acme-challenge/) is kept public so SSL issuance/renewal keeps working while a site is offline, and toggling re-runscertbot install(no new issuance, no rate-limit risk) so HTTPS is never dropped. The offline page is a shared, self-contained static page at/var/www/cipi-suspended/index.html, created on demand on first suspend;no-storecache headers andnoindex,nofollowkeep suspended pages out of caches and search engines. The suspended state is shown incipi app show(Status),cipi app list(yellow dot +(suspended)) andcipi domains(a yellow⏸ suspendedmarker per row plus a count in the footer), and is cleared automatically when the app is deleted. This unblocks the WHMCS module's Suspend/Unsuspend lifecycle, which previously had no Cipi endpoint to call (see cipi-sh/whmcs).
- Valkey install used the wrong package name —
setup.shand migration 4.5.6 installedvalkey, but on Ubuntu 24.04 the daemon package isvalkey-server(sourcevalkey; binariesvalkey-server+valkey-tools). On servers that had already updated to 4.5.6 the switch aborted with "valkey package not available" and leftredis-serverrunning (no data touched). Since 4.5.6 has already shipped and a migration can't be re-run,setup.shis corrected tovalkey-serverand migration 4.5.7 performs the full, corrected Redis → Valkey switch itself (self-contained: reuse password, preserve the RDB/AOF dataset, purgeredis-server, installvalkey-server+valkey-tools, restore data, rewriteserver.json/blacklist). It adds extra safety nets: it auto-enables theuniversecomponent when the package isn't found (viaadd-apt-repositoryif present, otherwise by editing the deb822/legacy APT sources directly — nosoftware-properties-commonneeded) and re-checks; it runs a post-start health check (PING→PONGwith the password) and, if Valkey doesn't come up healthy (or can't be installed), rolls back toredis-serverrestoring both the saved password and the dataset, so the server is never left without a working cache backend. The dataset snapshot is kept until the switch is verified, then cleaned up. Idempotent — servers already on Valkey skip it.
- Redis → Valkey — Cipi now provisions Valkey (the BSD-licensed, Redis-compatible fork shipped in Ubuntu 24.04 Universe as
valkey-server+valkey-tools) instead ofredis-server. Valkey speaks the same RESP protocol on the same port (127.0.0.1:6379) and honours the samerequirepass/binddirectives, so apps need zero changes: thephpredisextension and existingREDIS_*.envvalues keep working as-is.setup.shinstalls and configures Valkey (/etc/valkey/valkey.conf, servicevalkey-server);cipi service …managesvalkey-server(withredis-server/redis/valkeyaccepted as aliases); credentials live undervalkey_user/valkey_passwordinserver.json(legacyredis_*keys are still read as a fallback).cipi reset valkey-passwordreplacescipi reset redis-password(the old name stays as an alias). - Migration 4.5.6 — automatic Redis → Valkey switch for existing servers — on
cipi self-update, servers still runningredis-serverare migrated to Valkey reusing the current password (recovered fromserver.jsonor/etc/redis/redis.conf), so no app.envneeds editing. It first verifies thevalkeypackage is installable (so a server that can't obtain it keeps its working cache backend untouched), preserves the dataset (forces an RDBSAVEand snapshotsdump.rdb/AOF, restored into Valkey's data dir after install — Valkey reads the Redis 7.2 RDB/AOF format, so cache/sessions/queued jobs survive), then stops and purgesredis-server(avoiding any apt Conflicts/Replaces ambiguity between the two packages) before installing and configuring Valkey on the same port with the samerequirepass/bind, and rewritesserver.json(redis_*→valkey_*) and the unattended-upgrades blacklist. If the install fails it restoresredis-serverwith the saved password, so the server is never left without a cache backend; otherwise rollback is a plainapt install redis-server(the password lives inserver.json). The migration is idempotent and safe to re-run.
cipi domains— global domain/alias map — a new top-level command that lists every domain and alias across all apps in a single table: DOMAIN, owning APP, KIND (primary/alias), TYPE (Laravel/Custom), PHP version, DOCROOT (publicfor Laravel,/<docroot>for custom), Git BRANCH, LAST DEPLOY as a human-relative age (just now,10m ago,2h ago,3d ago,2w ago,2mo ago,2y ago— derived from the mtime of the app'scurrentsymlink, which Deployer atomically re-points on every successful deploy;-when never deployed), per-name SSL status (✓/✗, detected from/etc/letsencrypt/live/<domain>), and the Git REPOSITORY (or(SFTP only)for custom apps with no repo). Rows are sorted by domain and a footer summarises totals (domains, apps, certs), making it easy to audit the whole mapping or spot a domain that's still missing a certificate — complements the per-appcipi alias list <app>.llshell alias for app users — the.bashrcgenerated for every new app user (Laravel and custom) now definesalias ll='ls -al'for quicker directory listings over SSH. Migration 4.5.5 retro-fits existing apps: it appends the alias to each app's~/.bashrconce (only when missing, preserving ownership), so apps created before 4.5.5 get it on the nextcipi self-update.
- PHP < 8.3 can no longer be installed — Cipi now bundles Deployer 8 (the current major, downloaded as the latest
deployer.org/deployer.phar), which requires PHP >= 8.3. Becausedepis executed with the app's PHP version (/usr/bin/php${php_ver} /usr/local/bin/dep deploy …), running the v8 phar under PHP 7.4/8.0/8.1/8.2 would break every deploy. To prevent that mismatch at the source,validate_php_versionnow accepts only8.3,8.4,8.5, socipi php install,cipi php switch,cipi app createandcipi app editreject older versions with a clear message. The PHP recipes (lib/deployer/{laravel,custom}.php) are PHP-based and unchanged in Deployer 8, so no recipe migration is needed. Legacy installs are still detectable and removable:cipi php removeuses the newvalidate_php_version_knownhelper (7.4–8.5) so you can clean up a pre-4.5.4 server with e.g.cipi php remove 8.1.
- Deploy-time guard for apps still on PHP < 8.3 — restricting installs doesn't help servers that already host apps pinned to old PHP.
cipi deploy <app>andcipi deploy <app> --rollbacknow call_deploy_assert_php_compat, which checks the actually installed Deployer major (deployer_major_version, parsed fromdep --version): only when Deployer 8+ is present and the app's PHP failsvalidate_php_versiondoes it abort before invokingdep, with a clear "upgrade to 8.3/8.4/8.5" message instead of a cryptic phar parse error. Under Deployer 7 it's a no-op (those apps still deploy). Migration 4.5.4 performs the same check once at update time — gated on Deployer 8+ — and prints the list of affected apps so operators know exactly what to upgrade (it changes nothing).
- Panel API HTTP 500 after every self-update (the real root cause) —
lib/self-update.shrunschown -R root:root /opt/cipion each update (including the nightly 03:50 cron), which also re-roots the Laravel panel app under/opt/cipi/api. Withstorage/,database/andbootstrap/cache/ownedroot:root, PHP-FPM (www-data) can no longer openstorage/logs/laravel.logor write the SQLite DB — Laravel then fatals while trying to log the error (UnexpectedValueException: ... laravel.log ... Permission denied), so the browser only sees a bareHTTP ERROR 500. The compensatingwww-datare-chown lived inside thecipi-apipackage block, which is skipped when/opt/cipi/cipi-apiis absent (package installed from Packagist) — so the panel stayed broken after each update. This is what the 4.5.0/4.5.1 work (FPM pool, sessions, job/metrics pruning) never addressed, because those targeted symptoms, not the ownership reset. Fix:cipi self-updatenow callsensure_cipi_api_permissionsunconditionally right after the root chown, andlib/migrations/4.5.3.shrepairs already-broken servers (reclaimsstorage/database/bootstrap-cache/.envforwww-data, clears stale root-owned config cache, restarts FPM + queue). Immediate manual fix on an affected server:cipi api fix-permissions && systemctl restart php8.5-fpm.
- HTTP basic auth for apps — protect any app (Laravel or custom) behind an Nginx username/password prompt:
cipi basicauth enable <app> [--user=NAME] [--password=PASS],cipi basicauth disable <app>,cipi basicauth status <app>. Credentials are stored in/etc/nginx/cipi-basicauth/<app>.htpasswd(hashed withopenssl passwd -apr1, noapache2-utilsneeded); state lives inapps.json(basic_auth). Theauth_basicdirectives are injected into the app'slocationblocks by_create_nginx_vhost, so protection survives vhost regeneration (alias add/remove, PHP edit) and certbot clones it into the:443block — HTTPS is covered too. ACME challenges stay public (auth is injected per-location, not at server level), and toggling re-runscertbot install(no new issuance, no rate-limit risk) so HTTPS is never dropped. Removed automatically oncipi app delete. Distinct fromcipi auth(Composerauth.json).
- Panel API still 500s "after a while" — 4.5.1 wired up the panel scheduler and pruned
cipi-job-logs,cipi_jobs/failed_jobs, and the WAL, but missed the last unbounded-growth source: thecipi-apipackage'scipi:record-server-metricsruns every minute (~1440 rows/day) and no job ever prunes the metrics table. Within a few months it reaches hundreds of thousands of rows; the dashboard //apiserver endpoints full-scan it on every load until a query crosses PHP-FPM'srequest_terminate_timeout(300s), the worker isSIGKILL'd mid-request, and the browser gets an opaqueHTTP ERROR 500— intermittent at first, then constant.cipi-api-maintainnow prunes the server-metrics table to 14 days as well (table + timestamp column auto-discovered from the SQLite schema, so it stays correct across package versions), then runs thewal_checkpoint(TRUNCATE).lib/migrations/4.5.2.shretrofits existing servers: rewrites the maintenance helper and runs an immediate prune + WAL truncate so operators don't wait until 04:15. - Self-update backups filling
/opt—cipi self-updatecopied/opt/cipito/opt/cipi.bak.<timestamp>before every run (including daily cron @ 03:50) but never deleted old copies. Afterself-update, Cipi now keeps only the 7 newestcipi.bak.*directories and removes the rest.
-
Panel API: recurring 500s on
/and/api/*while user apps stayed up — 4.5.0 fixed the acute FPM saturation symptom, but the chronic cause was untouched: thecipi-apiLaravel package (v1.7.0+) registers scheduled commands (cipi:prune-job-logsdaily @ 03:30,cipi:record-server-metricsevery minute) and the cipi installer never wired up* * * * * php artisan schedule:runfor/opt/cipi/api— user apps had it via their per-user crontab; the panel did not. Over weeks of operation this produced:storage/app/cipi-job-logs/{uuid}.logaccumulating forever (one file per deploy / artisan / MCP /sudo cipi db …call invoked via the API), eventually exhausting disk space or inodes →fopen()failures surfaced as opaque 500s on the panel while per-app vhosts (separate pool, separate writes) kept serving.cipi_jobsandfailed_jobsrows accumulating forever in the panel SQLite, slowing every authenticated request and bloating WAL.database.sqlite-walgrowing unbounded —PASSIVEauto-checkpoints don't reclaim space under concurrent FPM + queue writers.- Laravel session files piling up under the default
filedriver. The welcome route'swebmiddleware writes one file per anonymous hit (bots, uptime monitors); GC is probabilistic (2/100) so low-traffic panels effectively never GC. Once the sessions dir grew large enough,scandir()stalled FPM workers pastrequest_terminate_timeout.
Fixed by wiring the Laravel scheduler into system cron for the panel app, adding a daily maintenance job for the SQLite cleanups, and switching the panel to the
arraysession driver so the welcome page no longer writes to disk per request.
/etc/cron.d/cipi-api— installed by_api_setup_cron(inlib/api.sh) and by migration 4.5.1 on existing servers. Two entries:* * * * *runsphp /opt/cipi/api/artisan schedule:runaswww-data(drives the cipi-api package's scheduled commands);15 4 * * *runs/usr/local/bin/cipi-api-maintaindaily for the SQLite cleanups./usr/local/bin/cipi-api-maintain— daily maintenance helper. Runsphp artisan queue:prune-failed --hours=336, deletescipi_jobsrows older than 14 days that arecompletedorfailed(running/pending are preserved), and runsPRAGMA wal_checkpoint(TRUNCATE)to reclaim WAL space.lib/migrations/4.5.1.sh— retrofits installed servers: lays down the two cron files, setsSESSION_DRIVER=arrayin.env, prunes accumulatedcipi-job-logs/*.logolder than 14 days, runs an immediatecipi_jobs+queue:prune-failed+ WAL truncate so operators see the benefit without waiting for 04:15, and reloadscron. Idempotent.- Logrotate for the maintenance log —
/etc/logrotate.d/cipi-api-maintainkeeps/var/log/cipi-api-maintain.logbounded (weekly, 8 rotations,copytruncate).
SESSION_DRIVER=arrayis now forced in/opt/cipi/api/.envon install/update/upgrade/migration (the panel is token-only; persistent sessions only added disk thrash).api_setup/api_update/api_upgradeall call_api_setup_cronand_api_ensure_session_driver_envnow, so any path through the API installer leaves the cron + env in the correct state.
- Panel API: silent 500 under load (no log written) — Long sync calls in the API (deploy, artisan, MCP,
sudo cipi …) saturated a too-small PHP-FPM pool (pm.max_children = 10); whenrequest_terminate_timeout = 300fired, FPMSIGKILL'd workers mid-request, so Laravel's exception handler never ran and nothing reachedstorage/logs/laravel.logorcipi-api-php-error.log— the only trace was injournalctl -u php<ver>-fpm. Fixed by enlarging the pool, adding a slowlog (PHP backtrace 30s before the kill), and capturing child stderr in the FPM log. - Panel API:
database is locked500s — SQLite defaultjournal_mode=DELETEplus Laravel's ~5sbusy_timeoutcaused contention between thecipi-queueworker and FPM children under burst traffic. Switched the panel SQLite DB toWAL+synchronous=NORMAL+busy_timeout=15000(set on everycipi api setup|update|upgradeand on existing servers via migration 4.5.0). cipi-queuememory drift — The systemd worker ranqueue:workindefinitely; long-lived PHP processes leak memory. Now restarted automatically every hour or 200 jobs (--max-time=3600 --max-jobs=200 --rest=1).
- PHP-FPM pool for the Panel API —
_api_create_fpm_pool(inlib/api.sh) andlib/migrations/4.5.0.shnow write a tuned pool:pm.max_children = 25,pm.start_servers = 4,pm.min_spare_servers = 2,pm.max_spare_servers = 8,pm.max_requests = 200,pm.process_idle_timeout = 60s,listen.backlog = 1024. Addedrequest_slowlog_timeout = 30withslowlog = /var/log/cipi-api-fpm-slow.logandcatch_workers_output = yesso child stderr/fatals land in the FPM log even when a worker dies before Laravel can log. The PHP childerror_logmoved from/var/log/nginx/cipi-api-php-error.log(logically Nginx's) to/var/log/cipi-api-php-error.log, registered in a newlogrotate.d/cipi-api-logs. - Nginx vhost for the Panel API —
_api_create_nginx_vhostnow sets explicit FastCGI buffers (fastcgi_buffers 16 32k,fastcgi_buffer_size 64k,fastcgi_busy_buffers_size 128k) and adds a local-onlylocation = /cipi-api-fpm-status(allow 127.0.0.1) used bycipi api statusto read pool stats. - Laravel logging defaults —
LOG_CHANNEL=stackandLOG_STACK=single,stderrare now forced in/opt/cipi/api/.envon install/update/upgrade/migration, mirroring errors to FPM stdout/stderr so they survive aSIGKILL'd worker. cipi api status— Now also prints active/idle FPM workers, listen queue depth, and slowlog hits (when present), so operators can see pool saturation at a glance.
lib/migrations/4.5.0.sh— Retrofits installed servers (auto-detects the PHP version actively running the API pool) by rewritingcipi-api.conf,cipi-queue.service, the Nginx vhost,.envlog channels, the SQLite pragmas, and the logrotate config. Runscertbot --nginx --reinstallwhen an SSL cert exists, so the redirect/443 block survives the vhost rewrite. Idempotent and safe to re-run.
cipi app delete— leftover Linux group blocking recreate — On app create,useraddcreates a private primary group named like the user andusermod -aG "$app" www-dataaddswww-datato that group.userdel -rremoves the user and home directory but does not always remove the matching group from/etc/group. Recreating the same app then failed withuseradd: group <app> exists.app_deletenow runsgpasswd -d www-data "$app"(dropwww-datafrom the group), thenuserdel -r, thengroupdel "$app". On servers already in a bad state, clean up manually withgpasswd -d www-data <app> 2>/dev/null; groupdel <app> 2>/dev/nullbeforecipi app create.
- App crontabs wiped by migration 4.4.14 — The
sedcommand in migration 4.4.14 used|as delimiter while the replacement string contained||(bash OR forcipi-app-notify).sedinterpreted the first|of||as end-of-replacement, failed to parse, and produced empty output — whichcrontab -u <user> -then installed as an empty crontab, wiping both the Laravel Scheduler (schedule:run) and the deploy trigger for every app. Fixed the 4.4.14sedto use#as delimiter. Migration 4.4.18 detects apps missingschedule:runand re-creates the full crontab (scheduler + deploy trigger + failure notification).
- Panel API database commands /
sudo: a terminal is required—/etc/sudoers.d/cipi-apiallowedwww-datato run onlycipi app|deploy|alias|sslandcat apps.json, notcipi db. Anysudo cipi db …from PHP (syncGET /api/dbsor queue jobs) therefore asked for a password and failed without a TTY. The whitelist now includesdb list,db create,db delete,db backup,db restore(two args), anddb password. New installs get this fromsetup.sh; existing servers via migration 4.4.17 (runs oncipi self-update).
-
Panel API readonly SQLite / log permission errors — Root-run
composer(e.g. duringcipi self-update) could leavedatabase.sqliteorstorage/logsowned by root beforemigrate, causing attempt to write a readonly database and Monolog Permission denied onlaravel.log. Addedensure_cipi_api_permissionsincommon.sh(chownstorage,database,bootstrap/cache→www-data, plus/opt/cipi/api/.env→www-data, mode640), invoked before API token commands,api status,api update, and after API install;cipi self-updatenow runs fullchownimmediately aftercomposer updateand runsvendor:publishaswww-data. New commandcipi api fix-permissionsfor manual repair.Migration 4.4.16also createswww-data’s PsySH config dir socipi api status(tinker) is reliable. -
cipi api statusappeared to hang after Domain — Status prints Queue first. Laravel and cipi-api versions are read fromcomposer.lockviajq(nophp artisan/composer showbootstrap). Pending job count usessqlite3on the panel SQLite DB whenDB_CONNECTION=sqlite;tinkeris only a fallback with a shorttimeout.timeoutstill applies to rare fallbacks;_api_ensure_psysh_homeon new installs. Servers already on 4.4.16 before this revision can runcipi api fix-permissionsonce (migration does not re-run). -
cipi api token createappeared to hang — The ability menu ran inside command substitution$(...), so prompts were written to stdout (captured) instead of the terminal;readwaited with no visible UI. Menu and prompt now go to stderr; input is read from/dev/ttywhen available.
cipi api token createabilities — The interactive ability list now matches the Cipi API docs: addeddeploy-manage,dbs-view,dbs-create,dbs-delete,dbs-manage, andapps-view. The whiptail fullscreen checklist is replaced by a plain-terminal checklist (numbered rows, ✓/· markers, toggle by number,a/n/Enter).
- Root crontab S3 backup/prune — all apps —
cipi backup configurenow appends default root cron lines that runcipi backup runandcipi backup prune --weeks=4(no per-app name), unless acipi backup runline already exists. Migration 4.4.15 rewrites existing root crontab entries fromcipi backup run <app>/cipi backup prune <app> --weeks=Nto the global form and drops duplicate lines after normalization.
- SMTP alerts for webhook/cron deploy failures — Deploys triggered by
.deploy-trigger(cipi/agent webhook) randep deployas the app user with no notification path; only interactivecipi deploycalledcipi_notify. Added/usr/local/bin/cipi-app-notify, invoked viasudofrom the deploy-trigger crontab on non-zero exit, which readssmtp.jsonas root and emails the last lines oflogs/deploy.log. New Laravel apps get an updated crontab line and sudoers rule; migration 4.4.14 updates existing apps.
- Root-owned Laravel logs (
shared/storage/logs/laravel-*.log) — Old logrotatecreate 0640 root rootleft rotated log files owned byroot:root. The app user could not read them, breakingspatie/laravel-backup(ZipArchive Permission denied) and other tools that access logs.ensure_app_logs_permissionsnow reclaims root-owned files in bothlogs/andshared/storage/logs/, restoring ownership to the app user. Migration 4.4.13 runs this for all apps.
- Deployer
writable_dirs:set()instead ofadd()— The Laravel recipe (recipe/laravel.php) already defineswritable_dirswithstorageandstorage/logs; usingadd()appended our list but kept those entries, sochmod -Rstill touchedlaravel-*.log. Changed toset('writable_dirs', [...])to fully override the recipe defaults. Migration 4.4.12 regeneratesdeploy.phpfor all Laravel apps.
- Deploy
deploy:writable/ chmod onstorage/logs/*.log— Deployer was runningchmod -Ronstorageandstorage/logs, so it tried to change mode on existinglaravel-*.logfiles (EPERM with ACLs or other attributes).writable_dirsnow lists only concrete subdirs understorage(app, framework, …), not the parentstorageorstorage/logs. A follow-up task setschmod 775on thestorage/logsdirectory only (no recursive file chmod).lib/deployer/laravel.phpis updated; migration 4.4.11 regenerates/home/*/.deployer/deploy.phpfrom the template for every non-custom app (overwrites localdeploy.php).
- Deploy
deploy:writable/ chmod onstorage/logs/*.log— Residual per-file ACLs on Laravel logs (from older Cipi) still caused Operation not permitted duringchmod.ensure_app_logs_permissionsnow strips file ACLs and default ACL onlogs/andshared/storage/logsbefore re-applying directory-only ACLs forcipi.cipi deploy <app>runs this automatically before Deployer. Migration 4.4.10 applies the same once on existing servers.
common.shdefaults vs readonlyCIPI_*— Replaced: "${CIPI_CONFIG:=...}"withif [[ -z "${CIPI_CONFIG:-}" ]]; then … fi(and the same forCIPI_LOG). If a server never completedself-updateafter 4.4.6,/opt/cipi/lib/common.shcould still contain a plainCIPI_CONFIG=…assignment on line 7; that blocks everycipirun until the file is replaced.- Stuck servers — If
cipi self-updatefails before fixingcommon.sh, run once as root:curl -fsSL https://raw.githubusercontent.com/cipi-sh/cipi/latest/lib/fix-common-readonly.sh | bash(seelib/fix-common-readonly.sh), thencipi self-update.
common.shvs readonlyCIPI_CONFIG/CIPI_LOG— The maincipibinary sets these asreadonlybefore sourcingcommon.sh; assigningCIPI_CONFIG=...caused readonly variable. Defaults now use: "${CIPI_CONFIG:=...}"/: "${CIPI_LOG:=...}"so existing readonly values are left unchanged and migrations still get defaults when unset.
- Migration /
common.shwhen sourced standalone — IfCIPI_LOGwas unset (e.g. migration 4.4.6 sourcingcommon.sh),mkdir -p "${CIPI_LOG}"expanded to an empty path.common.shnow defaultsCIPI_CONFIGandCIPI_LOGbefore loading the vault. - Deploy
deploy:writable/ chmod on Laravel logs — ACLs applied withsetfacl -Rand default ACLs onshared/storage/logscausedchmodto fail with Operation not permitted on existinglaravel-*.logfiles. Directory-only ACLs forcipiare kept; per-file and default ACLs on that tree are removed. Migration 4.4.7 clears those ACLs on existing servers and reapplies the corrected layout.
- App log access (
logs/, Laravelstorage/logs) — App home directories are750(app:app), so thecipiuser could not traverse/home/<app>/to read logs without root. Nginx vhost logs are written bywww-data; logrotate usedcreate 0640 root …, so after rotation files could be owned byrootand no longer writable/readable as intended. New installs and migration 4.4.6 setlogs/toapp:www-datawith setgid2775, apply ACLs socipican traverse the home and read logs, replace logrotatecreatewithcopytruncate(keeps correct ownership), and repair existing root-owned log files under/home/*/logs/.
ensure_app_logs_permissionsinlib/common.sh— Called fromcipi app createand sync app import so new apps get the same layout from day one.
- PHP Redis extension (phpredis) — The installer and
cipi php install <ver>now include theredispackage (php*-redis), so the phpredis extension is available for Laravel and other apps using the native Redis client - Migration 4.4.5 — On
cipi self-update, existing servers automatically installphp*-redisfor every PHP version already present (7.4–8.5 with FPM), then reload PHP-FPM
- Post-install summary — The final screen after
setup.shno longer prints the Stack block (Nginx, MariaDB, Redis, PHP, Node.js, Composer, Deployer versions); it goes from Server (IP, OS) straight to credentials and next steps
- Optional Git for custom apps — On
cipi app create --custom, the Git repository prompt can be left empty to provision SFTP-only hosting (no clone):htdocsis created with a placeholder page, branch is omitted, andcipi deployexplains that there is no repository until you set one withcipi app edit <app> --repository=....
cipi app create --custom— Creates a custom app with classic deploy (no zero-downtime): code is deployed intohtdocs(nocurrent/sharedsymlinks). During creation you only choose document root (default/, or e.g.www,dist,public). Nginx is fixed:index index.html index.php,try_files $uri $uri/ /index.php?$args,error_page 404 /404.html(no prompts for try_files or entry point). Custom apps have no database, no.env, no cron, no queue workers, no webhook; post-creation summary shows only SSH, deploy key, and next steps.
- App types —
cipi app createnow supports only Laravel (default) and--custom. cipi app show— Displays type "Custom" and docroot when applicable; Webhook line is shown only for Laravel apps.cipi app env— Exits with an error for custom apps (no .env).cipi app reset-db-password— Exits with an error for custom apps (no database).cipi app delete— Skips database drop for custom apps (none was created).
- Server IP in app creation summary —
cipi app createnow shows the server's public IP address right below the domain in the post-creation summary, making it easy to configure DNS records without leaving the terminal - MariaDB connection URL in app creation summary —
cipi app createnow displays a ready-to-usemariadb+ssh://connection URL after the database credentials; the URL includes SSH credentials, server IP, database credentials, and database name in a single copyable string (e.g.mariadb+ssh://user:sshpass@1.2.3.4/user:dbpass@127.0.0.1/user), useful for connecting from database clients like TablePlus, DBeaver, or Sequel Pro via SSH tunnel
- PHP 8.5 as sole pre-installed version — The installer now installs only PHP 8.5 instead of both 8.4 and 8.5; PHP 8.5 is the default CLI version and the runtime used by the Cipi API FPM pool; other PHP versions (7.4–8.4) can still be installed on demand via
cipi php install <version> - Default PHP for new apps set to 8.5 —
cipi app createnow defaults to PHP 8.5 when no--phpflag is provided
- MariaDB version "N/A" in post-install summary —
grep -oP(Perl regex) is not available in all environments; replaced with portableawkparsing and redirected stderr to stdout (2>&1) sincemariadb --versionmay write to stderr
- Self-update crash on version upgrade — The
cipimain script was read lazily by bash; whencipi self-updatereplaced the file on disk mid-execution, bash would resume reading the new file at the old byte offset, causingsyntax error near unexpected token ';;'whenever the new version had different line lengths (e.g. added commands); wrapped the entire script in a{ …; exit; }block so bash reads it fully into memory before executing, making on-disk replacement safe
cipi ban list— List all IPs currently banned by fail2ban, grouped by jailcipi ban unban <IP>— Unban a specific IP from all fail2ban jails
- Fail2ban hardening — Progressive banning with exponential backoff (24h base, doubles each time, 7-day cap); reduced max retries from 5 to 3; added
recidivejail that bans repeat offenders for 7 days after 3 bans in 24h; migration 4.3.0 upgrades existing installations automatically
cipi php switch <ver>— Switch the system default PHP version used by root/cipi; migrates the API FPM pool, restarts the API queue worker, and sends email notification;cipi php listnow shows which version is the system default;cipi php removenow blocks removing the system default version
- App commands use wrong PHP version — Deployer (
dep), Composer, and all deploy-related commands now run with the app's configured PHP version (/usr/bin/phpX.Y) instead of the system default; affectscipi deploy,cipi deploy --rollback, crontab deploy triggers,.bashrcaliases (deploy,composer), andcipi sync importdeploys; Deployer config now explicitly setsbin/composerto use the app's PHP; migration 4.2.9 patches all existing apps automatically
- MariaDB version detection — Installation summary showed the mysql client protocol number (e.g.
15.2) instead of the actual MariaDB server version; replaced deprecatedmysql --versionwithmariadb --versionand properDistribfield parsing
- su elevation notifications — Restricted
suemail alerts to only thecipi → rootescalation; all othersutransitions are now silently ignored to reduce noise
- Workers stuck in EXITED — Supervisor
autorestart=unexpected(introduced in 4.1.2) prevented workers from restarting after a graceful--max-timeexit (exit code 0); reverted toautorestart=truewhich is safe becausesupervisorctl stop(used during deploys) puts processes in STOPPED state, which Supervisor never auto-restarts regardless of theautorestartsetting
- Git clone non-interactive —
setup.shandself-update.shnow setGIT_TERMINAL_PROMPT=0ongit clonecommands to prevent credential prompts in automated/piped environments - Sudo notification spam during deploy — PAM auth notifications were triggered when app users executed
sudo cipi-worker stop/restartduring deploys;_is_internal()now detects/usr/local/bin/cipicommands and Deployer (dep) in the process tree, suppressing notifications for all cipi-initiated sudo operations
- Official repo only — removed
andreapollastri/cipifallback fromsetup.shandself-update.sh; all references now point exclusively tocipi-sh/cipi
If you have issues with cipi self-update after 4.2.5, run:
sed -i 's/^ git clone/ GIT_TERMINAL_PROMPT=0 git clone/' /opt/cipi/lib/self-update.sh
- GitHub organization migration — moved repos to cipi-sh organization; Composer package names updated from
andreapollastri/cipi-api→cipi/apiandandreapollastri/cipi-agent→cipi/agent - Self-update & installer fallback —
setup.shandself-update.shnow trycipi-sh/cipifirst, falling back toandreapollastri/cipifor backward compatibility during the main repo transition - Migration 4.2.5 — automatically migrates existing installations: replaces old Composer package in the API app and updates crontab references
Cipi has been moved to organization namespace. If you have issues within self-update command after this version, run:
sed -i 's/^ git clone/ GIT_TERMINAL_PROMPT=0 git clone/' /opt/cipi/lib/self-update.sh to fix it!
- Centralized security event log — all security-relevant events (SSH key changes, app lifecycle, password resets, sudo/su/SSH login, cron failures) are always logged to
/var/log/cipi/events.login a compact one-line format, regardless of whether SMTP is configured;log_event()helper incommon.shand inline logging in PAM and cron notification scripts suPAM notifications — PAM auth notification now coverssuin addition tosudoandsshd; alerts include who ransu, the target user, SSH key, and client IP; PAM rule added to/etc/pam.d/suin bothsetup.shand migration4.2.3.sh- Client identity in all notifications — every email notification sent via
cipi_notify()now includes a footer with the client IP (SSH_CLIENT) and the SSH key name used to authenticate; key name is resolved viaSSH_USER_AUTHwithauth.logfallback - Sudo command in notifications — sudo alerts now include the command that was executed (
SUDO_COMMAND) - SSH key rename notification — email alert when an SSH key is renamed; includes old name, new name, fingerprint, server hostname, and timestamp
- SSH key fingerprint resolution —
SSH_USER_AUTHcontains raw key data (type base64), not a fingerprint; fixed_resolve_ssh_key_name()(PAM script),_get_session_fingerprint()(ssh.sh) and_get_session_key_name()(common.sh) to reconstruct the fingerprint viassh-keygen -lf -instead of reading field 3 directly - Email
\nliteral —_smtp_sendnow usesprintf %binstead of%sfor the body so escape sequences are interpreted correctly - Backup S3 region handling —
_aws_s3()now passes--regionfrombackup.json(defaults toeu-central-1); fixesNoneType is not iterableerrors on S3-compatible APIs when region is empty - Crontab setup error —
setup_cronno longer fails when no existing crontab is present (|| trueguard oncrontab -l) - Installer resilience —
setup_pamandsetup_cronfailures no longer abort the entire installation; errors are logged with a warning and setup continues
- Privileged-to-inferior suppression — PAM auth notifications from
cipi/roottowards non-sudo app users are now suppressed unless the action is part of an app create/edit/delete lifecycle operation; reduces noise from routine app provisioning - Sync push improvements — uses cipi's ed25519 sync key explicitly (
-i /home/cipi/.ssh/id_ed25519); rsync failure gracefully falls back to scp; remote Cipi version checked viasudo cipi versioninstead of reading/etc/cipi/version; export suppresses manual transfer instructions during push; archive cleaned up after successful import;scpexamples updated to usecipiuser - SSH key rename logging —
log_actionnow includes old and new key name for rename operations
- SSH login notification showing "SSH Key: unknown" — PAM auth notification script could not resolve the SSH key name on login because
SSH_USER_AUTHis not yet available in the sshd PAM session context; added fallback that parses/var/log/auth.logfor theAccepted publickeyfingerprint and matches it againstauthorized_keysto resolve the key comment/name - Email notifications literal
\n— all notifications sent viacipi_notify()showed literal\ninstead of line breaks; fixed_smtp_sendto useprintf %bfor the body so escape sequences are interpreted correctly
- SSH key rename notification — email alert via SMTP when an SSH key is renamed; includes old name, new name, fingerprint, server hostname, and timestamp
- Client identity in all notifications — every email notification now includes a footer with the client IP (
SSH_CLIENT) and the SSH key name used to authenticate; key name is resolved viaSSH_USER_AUTHwithauth.logfallback suelevation notification — PAM auth notification now coverssuin addition tosudoandsshd; alerts include who ransu, the target user, SSH key, and client IP- Security event log — all notification events (SSH key changes, app lifecycle, password resets, sudo/su/SSH login, cron failures) are always logged to
/var/log/cipi/events.login a compact one-line format, regardless of whether SMTP is configured; rotated daily with 1-year retention via existing logrotate config
- Nginx default host 404 — requests to unconfigured domains (e.g. server IP with random paths) now always serve the "Server Up" page instead of the default nginx 404 error; uses
rewriteinstead oftry_filesfor reliable catch-all behavior cipi ssh list/cipi ssh removesilent exit — both commands printed the header but no keys; caused by((i++))returning exit code 1 wheni=0(post-increment evaluates to 0 = falsy) underset -euo pipefail; fixed with|| trueguard on all arithmetic increments- SSH key comment stripped on setup —
collect_ssh_key()usedawk '{print $1, $2}'to sanitize input, discarding the comment field (third+ column); keys added during install were always stored without their original comment
- PAM auth notifications — now include SSH key fingerprint/comment for both sudo and SSH login alerts; key is resolved via
ExposeAuthInfo+SSH_USER_AUTH - SSH access model — replaced
AllowUsers cipiwith group-based access (AllowGroups cipi-ssh cipi-apps);cipiuser remains key-only; app users can now SSH directly with username and password viaMatch Group cipi-appsblock that enablesPasswordAuthenticationselectively
- App lifecycle notifications — email alerts on app create, edit, and delete; includes server hostname, app name, domain, PHP version, and change details; sensitive data (passwords, tokens, keys) is never included
cipi app reset-password <app>— regenerate the SSH password for an app's Linux user; displays new password once and sends email notificationcipi app reset-db-password <app>— regenerate the MariaDB password for an app user; automatically updatesDB_PASSWORDin the app's.envfilecipi reset root-password— regenerate the root SSH password and updateserver.jsonin the vaultcipi reset db-password— regenerate the MariaDB root password and updateserver.jsonin the vaultcipi reset redis-password— regenerate the Redis password, restart Redis, and updateserver.jsonin the vault; warns about updating app.envfilescipi ssh rename [number] [name]— set or change the display name of an SSH key; updates the comment field inauthorized_keys; interactive selection if called without arguments
- Sudoers hardening —
www-datasudo access restricted from wildcard (cipi *) to an explicit whitelist of API commands only (app create/edit/delete,deploy,alias add/remove,ssl install,cat apps.json); prevents privilege escalation from a compromised PHP process - Command injection fix — replaced unsafe
evalwithprintf -vinread_input()andparse_args()(common.sh); user input is no longer interpreted by the shell - Sed injection fix —
branchandrepositoryvalues are now escaped before interpolation insedcommands (app.sh); prevents injection via special characters (|,&,\) - API command whitelist —
CipiCliServicenow validates commands against anALLOWED_COMMANDSwhitelist before executingsudo cipi; provides defence-in-depth alongside sudoers
- Non-interactive SSH key input —
setup.shnow acceptsSSH_PUBKEYenvironment variable for non-interactive installs (e.g.SSH_PUBKEY="ssh-rsa ..." bash setup.sh) - Random root password — installer generates a 32-character random root password, saves it in server.json, and displays it in the final summary
- SSH key setup instructions — clearer installer prompt: shows accepted key formats (ssh-rsa, ssh-ed25519, ecdsa) for existing keys, and RSA 4096 generation command for new keys
surestricted to sudo group — application users can no longer usesuto elevate to root or cipi (viapam_wheel.so group=sudo)
- SSH key paste in
curl | bash—readnow reads from/dev/ttyso interactive input works when setup is piped via curl - SSH key sanitization — automatically strips comments, carriage returns, and extra whitespace from pasted keys before validation
- SSH service restart on Ubuntu 24.04 — use
sshservice name withsshdfallback for compatibility across distributions - server.json missing during SSH hardening — installer now creates
/etc/cipi/server.jsonbefore writing to it, and MariaDB setup merges instead of overwriting
- SSH hardening at install —
setup.shnow asks for an SSH public key during installation (before any package install begins); creates a dedicatedcipiuser as the only SSH entry point; disables root login and password authenticationPermitRootLogin no,PasswordAuthentication no,PubkeyAuthentication yes,AllowUsers cipi,MaxAuthTries 3,LoginGraceTime 20,X11Forwarding no,ExposeAuthInfo yescipiuser has passwordless sudo for/usr/local/bin/cipi *only- Server-to-server ed25519 keypair auto-generated for sync operations
cipi ssh list— list all authorized SSH keys for the cipi user with fingerprint, comment, and current-session marker (<< current session)cipi ssh add [key]— add an SSH public key (interactive prompt if no argument); validates format, rejects duplicates; sends email notification via SMTP if configuredcipi ssh remove [n]— remove an SSH key by number (interactive list if no argument); sends email notification via SMTP if configured- Session safety — detects the key used for the current SSH session (via
ExposeAuthInfo+SSH_USER_AUTH) and blocks its removal - Last-key safety — prevents removing the last remaining key to avoid lockout
- Session safety — detects the key used for the current SSH session (via
cipi sync pubkey— display this server's sync public key (for server-to-server trust)cipi sync trust— add a remote server's public key to cipi's authorized_keys, enabling passwordlesscipi sync pushbetween servers- SSH key change notifications — email alerts (via existing SMTP) on every key add/remove, including server hostname, IP, key fingerprint, comment, timestamp, and remaining key count
- Sync default user —
cipi sync pushnow connects ascipi(wasroot); remote commands usesudo cipifor privilege escalation - Sync troubleshooting — updated help messages to reference
cipi sync trustandcipi sync pubkeyinstead ofPermitRootLogin yes - Installation summary — now shows SSH access info (login command, root-login disabled, password-auth disabled) and the server sync public key
- Sudoers —
SSH_USER_AUTHenv variable preserved through sudo (env_keep) for session key detection - Nginx default vhost — all requests to the server IP now serve the "Server Up" page instead of returning nginx default 404; custom
error_pagedirective catches all error codes (400–504) and serves/index.html, preventing nginx version leaks in error pages
- Worker restart loop during deploy — Supervisor no longer floods logs with
Could not open input file: /home/<app>/current/artisanduring deployments. Root cause: the worker process exited when thecurrentsymlink was briefly unavailable duringdeploy:symlink, triggering immediate Supervisor restarts before the new release was in place. Fix: Deployer now stops workers (workers:stop) before the symlink swap and restarts them after (workers:restart), ensuring zero restart attempts against a broken symlink cipi worker stop <app>— new CLI subcommand to cleanly stop all Supervisor workers for an app without removing their configurationcipi-worker stop— extended the sudoers-restricted helper to supportstopaction, enabling Deployer tasks to stop workers during deploy without elevated privileges- Supervisor
autorestart=unexpected— new worker configs now only auto-restart on unexpected exits withstartretries=5andstartsecs=3, reducing noise from transient failures - Sudoers —
cipi-worker stop <app>added to the sudoers whitelist in bothapp createandcipi sync
- Security auth notifications — email alerts on sudo elevation and privileged SSH logins (requires SMTP configured):
- Sudo: notifies when any user successfully elevates to root via
sudo, including who ran it and from which TTY - SSH login: notifies when
rootor any sudoer logs in via SSH, including source IP - Integrated via PAM (
pam_exec.so); runs asynchronously to avoid login delays; fails silently if SMTP is not configured
- Sudo: notifies when any user successfully elevates to root via
- Auth notifications: suppress internal sudo events — sudo notifications triggered by Cipi internal operations (API calls via PHP-FPM, queue workers, cron jobs, systemd services) are now silently skipped; only interactive sudo elevations from real SSH sessions generate alerts
- Detection via kernel
loginuid(primary) with process-tree inspection fallback (php-fpm, artisan queue, supervisord, cipi-queue)
- Detection via kernel
- Auth notifications: resolve "User: unknown" — the
SUDO_USERfield in sudo alerts now correctly resolves the calling user vialoginuidwhen the PAM environment does not propagate$SUDO_USER
- Vault readonly guard —
vault.shcould crash withreadonly variableerror when sourced multiple times in the same shell (e.g. during PAM hooks or nested cipi calls)
- Sync: export/import/list — transfer apps between CIPI servers
cipi sync export [app ...] [--with-db] [--with-storage]— export all apps or specific ones to a portable.tar.gzarchive including configs, SSH keys, deployer config, supervisor workers, and optionally database dumps and shared storagecipi sync import <file> [app ...] [--deploy] [--yes]— import apps from an archive into the current server; recreates users, databases (with new credentials), nginx vhosts, PHP-FPM pools, supervisor workers, crontabs, and deployer configs; selectively import specific apps from a multi-app archivecipi sync push [app ...] [--host=IP] [--port=22] [--with-db] [--with-storage] [--import]— export, transfer via rsync/scp to a remote server, and optionally run import on the remote; interactive prompts for SSH host/port with connectivity test and remote Cipi version checkcipi sync list <file>— inspect archive contents without importing (apps, PHP versions, DB/storage inclusion)--updatemode for import — when an app already exists on the target, incrementally syncs .env (preserving local DB credentials), database dump (drop + reimport), shared storage, supervisor workers, deployer config, nginx vhost (alias changes), and PHP version changes; new apps are created as before;push --importuses--updateautomatically- Pre-flight checks on import: warns about missing PHP versions, blocks import of apps that already exist (unless
--update); domain conflict check — blocks import if domain or alias is already used by another app on target or by another app in the same import batch .envDB credentials automatically updated on import with the new server's values- SSH deploy keys preserved from source (same key works with git provider)
- Email notifications (optional) — receive alerts when backup or deploy fails
cipi smtp configure— interactive SMTP setup (host, port, user, password, from/to, TLS); supports Gmail, SendGrid, Mailgun, etc.; installsmsmtpon first usecipi smtp status— show if notifications are enabled and recipientcipi smtp test— send a test emailcipi smtp disable/cipi smtp enable— toggle notifications without losing configcipi smtp delete— remove SMTP config- Notifications sent automatically on: backup errors (per-app or full run), deploy failures, system cron failures (self-update, SSL renewal)
cipi-cron-notifywrapper — runs system cron jobs and sends email alert on failure- Config stored in
/etc/cipi/smtp.json;smtp.jsonincluded in sync export for migration - Vault: config encryption at rest — all JSON config files (
server.json,apps.json,databases.json,backup.json,smtp.json,api.json) are encrypted on disk with AES-256-CBC using a per-server master key (/etc/cipi/.vault_key); transparent read/write with backward compatibility for existing plaintext configs; existing servers are automatically migrated on update - apps-public.json — plaintext projection of
apps.jsoncontaining only non-sensitive fields (domain, aliases, php, branch, repository, user, created_at); automatically regenerated on every app change; thecipi-apigroup reads this file instead of the encryptedapps.json, so the vault key stays root-only with no privilege escalation - Encrypted sync export —
cipi sync exportnow encrypts the archive with a user-provided passphrase (AES-256-CBC);cipi sync importandcipi sync listtransparently detect and decrypt encrypted archives; protects SSH keys,.envfiles, database dumps, and credentials during transfer; all sync commands accept--passphrase=<secret>for non-interactive/automated usage (cron, scripts) - GDPR-compliant log rotation — automatic retention policies via logrotate:
- Application logs (Laravel, PHP-FPM, workers, deploy, Cipi system) — 12 months
- Security logs (fail2ban, UFW firewall, auth) — 12 months
- HTTP / Navigation logs (nginx access & error) — 90 days
- apps.json isolation: app users could read other apps' webhook tokens via shared
www-datagroup membership. Introduced dedicatedcipi-apigroup — onlywww-data(PHP-FPM) belongs to it, so app SSH users can no longer access/etc/cipi/apps.json
ensure_apps_json_api_access()now creates and uses acipi-apigroup instead of relying on thewww-datagroup directly- Migration
4.0.8.shfixes permissions on existing servers and restarts PHP-FPM to pick up the new group - API
.envnow defaults toAPP_ENV=productionandAPP_DEBUG=falseon fresh install and upgrade - MOTD updated to "Easy Laravel Deployments"
- Git provider integration:
cipi git— automatic deploy key and webhook configuration for GitHub and GitLab repositories cipi git github-token <token>— save GitHub Personal Access Token for auto-setupcipi git gitlab-token <token>— save GitLab Personal Access Token for auto-setupcipi git gitlab-url <url>— configure self-hosted GitLab instance URLcipi git remove-github— remove stored GitHub tokencipi git remove-gitlab— remove stored GitLab token and URLcipi git status— show configured providers, tokens (masked) and per-app integration status- Auto-setup on
cipi app create: when a GitHub/GitLab token is configured, Cipi automatically adds the deploy key and creates the webhook on the repository — zero manual configuration needed - Auto-migrate on
cipi app edit --repository=...: when changing repository, Cipi removes deploy key + webhook from the old repo and creates them on the new one - Auto-cleanup on
cipi app delete: Cipi removes deploy key + webhook from the repository before deleting the app - New
lib/git.shmodule with GitHub REST API v3 and GitLab REST API v4 integration (deploy keys + webhooks CRUD) apps.jsonextended with optionalgit_provider,git_deploy_key_id,git_webhook_idfields per appserver.jsonextended on-demand withgithub_token,gitlab_token,gitlab_urlfields
cipi app shownow displays git provider integration status (provider, deploy key ID, webhook ID)cipi deploy <app> --keyshows "auto-configured" status when deploy key was added via APIcipi deploy <app> --webhookshows "auto-configured" status when webhook was added via APIcipi app createsummary adapts: shows "auto-configured" badge when git integration succeeded, or manual instructions with a setup tip when no token is configured- Graceful fallback: if no token is configured or the API call fails, Cipi falls back to manual setup (existing behavior) without interrupting the flow
- Global API:
cipi api <domain>— configure API at root (e.g. api.miohosting.it), no aliases - API SSL:
cipi api ssl— install Let's Encrypt certificate for API domain - API tokens:
cipi api token list|create|revoke— manage Sanctum tokens (abilities: apps-view, apps-create, apps-edit, apps-delete, ssl-manage, aliases-view, aliases-create, aliases-delete, mcp-access) - REST API (Bearer token):
GET/POST/PUT/DELETE /api/apps,GET/POST/DELETE /api/apps/{name}/aliases,POST /api/ssl/{name},GET /api/jobs/{id} - Async job system: all write operations (create, edit, delete, SSL, alias add/remove) dispatch background jobs via Laravel queue, returning
202 Acceptedwith ajob_idfor polling; GET operations remain synchronous - Sync validation: domain uniqueness, app existence, PHP version, username format and domain format validated synchronously before job dispatch (409/404/422 returned immediately)
- Swagger/OpenAPI docs at
/docs— interactive API documentation via Swagger UI (spec v2.0.0) - MCP server at
/mcp— requiresmcp-accessability, tools for app/alias/SSL management (async dispatch with job_id) - Dedicated PHP-FPM pool
cipi-apifor the API (isolated from app pools, up to 10 workers) - Queue worker
cipi-queuesystemd service for processing background jobs (auto-restart, 600s timeout) andreapollastri/cipi-apiComposer package: all API logic (controllers, services, models, MCP tools, migrations, views, routes) is now a standalone Laravel package, publishable on Packagist — install viacipi api <domain>orcomposer require andreapollastri/cipi-api- Welcome page
welcome.blade.php— dark/light theme landing page served at/ cipi api update— soft update:composer updateon all packages (Laravel minor/patch + cipi-api), re-publishes assets and runs migrationscipi api upgrade— full rebuild: freshcomposer create-project laravel/laravel+composer require cipi-api, preserves.env, database, SSL certificates and tokens; keeps old version at/opt/cipi/api.oldfor rollbackcipi api status— shows current Laravel version, cipi-api version, queue worker status and pending jobs
cipi app delete <app> --force— skip confirmation for non-interactive use- API read endpoints (GET /apps, GET /apps/{name}, GET /aliases) now read directly from
apps.jsoninstead of invoking CLI - API install uses
composer create-project laravel/laravel+composer require andreapollastri/cipi-apiinstead of overlay copy — easier upgrades to future Laravel versions - apps.json API access: when
cipi api <domain>is run, Cipi automatically configures/etc/cipiandapps.jsonso that www-data (PHP-FPM) can read them — no manualchmod 644or sudoers rules needed
- Nginx "conflicting server name" warnings when domains or aliases were duplicated
_create_nginx_vhostnow deduplicates domain + aliases before writingserver_name- Primary domain excluded from aliases when reading (handles legacy data where primary was added as alias)
cipi app createrejects creation if the domain is already used by another appcipi alias addrejects adding an alias that equals the primary domain or is already used by another appcipi ssl installexcludes primary domain from aliases when building Certbot-dflags
- Redis in the default stack — installed with password, bind to localhost only
cipi servicenow includesredis-server(list, restart, start, stop)- Redis credentials (user, password) saved in
/etc/cipi/server.jsonand shown at end of installation - Migration 4.0.4 for existing servers: installs Redis and adds
redis-serverto unattended-upgrades blacklist
- Redis added to unattended-upgrades package blacklist (managed by Cipi, no auto-upgrade)
cipi app logsnow includes Laravel daily logs (laravel-YYYY-MM-DD.log) fromshared/storage/logs/- Added
--type=laraveloption to tail only Laravel application logs
cipi deploy <app> --trust-host=<host[:port]>— trust a custom Git server by scanning and persisting its SSH host key- Custom Git server support in the deploy workflow (non-GitHub/GitLab repositories)
cipi backup prune [app] --weeks=N— delete S3 backups older than N weeks, per-app or globally
- Installer (
setup.sh) fix for edge cases during provisioning
- Documentation updated to reflect new deploy and backup commands
- Self-update mechanism revised: version check and install script updated
- Installer link updated to new canonical URL (
cipi.sh/setup.sh) - README refreshed (condensed, up-to-date command reference)
Complete rewrite of the Cipi CLI from the ground up.
- New modular shell architecture: each domain split into its own library (
lib/app.sh,lib/deploy.sh,lib/db.sh,lib/backup.sh,lib/ssl.sh,lib/php.sh,lib/firewall.sh,lib/service.sh,lib/worker.sh,lib/self-update.sh,lib/common.sh) lib/common.sh— shared helpers:parse_args,validate_*,generate_password,read_input,confirm,log_action, and app registry helpers (app_exists,app_get,app_set,app_save,app_remove)cipi service list|restart|start|stop— full service management for nginx, mariadb, supervisor, fail2ban, php-fpmcipi deploy <app> --unlock— unlock a stuck Deployer processcipi deploy <app> --webhook— display webhook URL and tokencipi deploy <app> --key— display the SSH deploy public keycipi deploy <app> --releases— list available releasescipi deploy <app> --rollback— roll back to the previous releasecipi backup configure— interactive S3 configuration wizardcipi backup run [app]— run backup for a specific app or all appscipi backup list [app]— list S3 backups per-app or globally (supports multiple S3 providers)cipi self-update [--check]— update Cipi in place;--checkshows available version without installingcipi app artisan <app> <cmd>— run arbitrary Artisan commandscipi app tinker <app>— open Laravel Tinker for an appcipi app logs <app> [--type=nginx|php|worker|deploy|laravel|all]— tail app logs by type- PHP 8.4 and 8.5 support
lib/cipi-worker— standalone helper script for queue worker management via sudoers- Nginx security headers (
X-Frame-Options,X-Content-Type-Options,Referrer-Policy) in generated vhosts - App registry stored as JSON (
/etc/cipi/apps.json) with full CRUD viaapp_save/app_remove - Structured action logging to
/var/log/cipi
cipi app createnow provisions: Linux user, directories, SSH deploy key, MariaDB database,.env, PHP-FPM pool, Nginx vhost, Supervisor worker, crontab (scheduler + deploy trigger), Deployer config, sudoers entry- Deployer recipe uses
recipe/laravel.phpwith automaticartisan:migrate,artisan:optimize,artisan:storage:link,artisan:queue:restart, andworkers:restarthooks cipi app editsupports--php,--branch,--repositoryflags and updates all affected config files atomicallycipi app deleteperforms full cleanup: workers, nginx, php-fpm, database, crontab, sudoers, SSL certificate, home directorycipi alias add/removeregenerates the Nginx vhost and reloads nginxcipi dbcommands (create,list,delete,backup,restore) rewritten with MariaDB-native toolingcipi ssl installuses Certbot with all aliases included in the certificate SANcipi php installmanages PHP-FPM installs per versioncipi firewall allow/listwrapsufw- Removed legacy
lib/commands.sh,lib/domain.sh,lib/nginx.sh,lib/database.sh - Removed Redis dependency from the default stack
- SSL Certbot integration with multi-domain vhosts
- Worker restart via supervisor with app-scoped naming
- PHP-FPM pool
open_basedirset correctly per app - Deploy key
authorized_keysandknown_hostspermissions hardened