SectionRoadmap
Roadmap, test matrix, CI
Stage 0 — foundation (done 2026-09-05, verified on Ubuntu 24.04)
- Repository, Go build (
make build),nfpm.yaml, units, sysusers/tmpfiles,install.sh,mp setup. - OS Profile for Debian/Ubuntu and EL9/EL10 (+ generic for dev machines); agent with
ApplyConfigSet,EnsureGroup,EnsureUnixUser,EnsureDirs,Service,Pkg; peer-cred on both sockets. - SQLite schema and migrations; auth (argon2id, sessions, Bearer tokens, CSRF check); job runner with locks; SSE.
- Installing nginx from nginx.org (
mp stack install nginx), nginx/Apache/php-fpm templates with golden tests, CLI and TUI. - ACME (lego, HTTP-01 via webroot):
mp ssl issue/list/renew/rm, thecertificatestable, automatic renewal, hot swap of the panel's certificate (mp web tls). Verified by issuing a production Let's Encrypt certificate on the test host. - Building the SvelteKit app (needs Node; for now a placeholder
web/build/index.html). - A deb/rpm repository and package signing.
- Installing PHP (Sury/Remi) and Percona/MySQL 8.4 with
auth_socket— moved to the start of stage 1. - VM matrix: a Proxmox testbed with nine machines covering the whole OS matrix,
make testbed-matrix(2026-09-09, see docs/08-testbed.md). Not wired into CI yet — that needs a self-hosted runner with access to the host.
Stage 1 — MVP (done 2026-09-05, verified on Ubuntu 24.04)
- PHP 5.6–8.5 via Sury/Remi (
mp php), several versions side by side. - Sites in modes A (nginx → php-fpm), B (nginx → Apache → php-fpm) and proxy; per-site php_value; ACL; placeholder page; automatic certificate.
- Apache 2.4 as a component (Debian/Ubuntu). EL — not verified.
- Databases: Percona/MySQL 8.4 with
auth_socket, databases and users, legacy authentication for PHP < 7.4. phpMyAdmin — not done. - Cron, firewall (nftables) + fail2ban, restic backups, metrics, logs,
mp doctor, DNS-01, TUI parity, web UI (SvelteKit), ru. - The full test matrix in CI on 9 OSes (needs a self-hosted runner with VMs).
Stage 2 — v1.0 (partial, 2026-09-05)
- File manager through the helper (
mp files, API/files), SFTP chroot, unix passwords. - Webhooks (HMAC), TOTP 2FA, API tokens, config history (
confhistory), template overrides,mp doctor. - Own PHP builds, a terminal in the browser, quotas, self-update, a WHMCS module, WebAuthn, a confined SELinux domain.
Automated testing (2026-09-06)
- Fake agent
internal/agent/agenttest: a unix socket, typed responses, a record of every call — covers the panel's jobs (sites, users, presets) without root or systemd. - Site pipeline tests: nginx/pool rendering, CMS presets and their PHP values,
php_initaking priority over the preset, allow-list, validation rejections, suspend, removal, custom nginx directives with rollback, cascading user removal. Coverage ofinternal/api9.5% → 21.2%, 23.3% overall. - Readiness timeouts moved into
Server.SetReadinessWaits: tests do not wait for nginx or the php-fpm socket (the suite runs in ~2 s). -
make check(fmt + vet + lint + test),make test-race,make cover,make web-check,make help. -
golangci-lintv2.13.2 with.golangci.yml; deliberately ignored errors are marked//nolint:errcheckwith a reason, real findings (S1017, S1009, ineffassign, unconvert) are fixed. -
scripts/check-templates.sh: golden configs are checked by realnginx -tandapachectl -t(CI installs nginx-core and apache2). -
e2e/(tage2e): a scenario against a live panel — account, site with a preset, PHP answers, the ban on PHP in uploads works, database, removal; cleans up after itself. Token or login/password via environment variables. - Tokens over the local socket:
POST /tokensacceptsuser, root without an account gets a token for the single administrator (with several — an error with the list), an administrator sees and revokes the tokens of any account;make e2e HOST=…mints a token over ssh and revokes it after the run. - CI: parallel jobs go / lint / templates / web, module and pnpm cache,
-racewith coverage in the summary, amd64 + arm64 builds, e2e viaworkflow_dispatch.
Managing PHP extensions (2026-09-06)
-
GET|POST /php/versions/{v}/extensions,mp php ext list|enable|disable, toggles in the branch card on the PHP page. The state is read from the branch'sconf.d(phpquery answers from the Debian registry and keeps showing a module afterphpdismod), the list of available ones frommods-available. After a toggle the branch's php-fpm is restarted. - Only for a whole branch: php-fpm is one master per version, pools inherit its extensions, an extension cannot be disabled for a single site. The interface says so plainly.
- Extensions a typical site does not work without are marked and need confirmation; nothing is forbidden — the administrator is root anyway.
- The agent gained a
dir/listoperation with an allow-list of directories (/etc/php/*/mods-available,*/conf.d,/etc/opt/remi/php*/php.d): without it the panel sees only the loaded modules, not the disabled ones.
PHP 8.5 (2026-09-06)
- Installing 8.5 failed: the panel asked for
php8.5-opcache, which Sury does not have — starting with 8.5, opcache is built into the core (php8.5 -mshows Zend OPcache,opcache.enable => On). A separate package exists only for 7.0 through 8.4; the rule and a test for both boundaries are ininternal/osprofile/php.go.
Adapting the web UI to phones and tablets (2026-09-06)
- Layout: below
lgthe 240px-wide sidebar turns into a slide-out drawer with a backdrop and a header with a button; above it, the old column stays. Following an item closes the menu, tap targets are larger. - Tables: one technique for all nine pages, because they all use the
.tblclass. Below 40rem a row becomes a card, the column headers move into labels on the left (data-labelon the cells), and the table stops being a table (display: block) — otherwise it sizes its width by the content and long values run off the edge. Cells without a label — the action buttons — stay on the right. - Small things: a full-width toast on phones, wrapping of a long docroot, the file manager toolbar on two lines, smaller page padding on a narrow screen.
- Checked at 375×812 and 768×1024 on every page: nothing runs off the screen horizontally anywhere, including the Monaco editor and the site page with its tabs.
File manager in the web UI (2026-09-06)
-
FileManager.svelte: browsing the account's home directory, breadcrumbs, jumping to a site's directory, creating a folder and a file, upload by drag-and-drop and by picking files, download, rename, permissions, archive extraction, multiple selection and deletion — all on top of the existing/filesAPI, that is, as the owner through the helper. - Text file editor: line numbers, Ctrl+S, Tab, a guard against leaving with unsaved changes. Binary files (by extension or a zero byte) and files over 1 MB open for download only. No syntax highlighting — it would mean a new npm dependency.
- The
/filespage (with an account picker for the administrator) and the "Files" tab on the site page, which opens in the site's docroot. - The editor is Monaco, the same core as in VS Code (
web/static/monaco, MIT): highlighting for php/html/css/js/ts/json/xml/ini/shell/sql/yaml/markdown/python/dockerfile, find and replace, multiple cursors, folding, the command palette, the theme follows the panel's theme, the height follows the content. The build is trimmed from 24 MB to 4.7: no language services (the json/css/html/ts IntelliSense workers), no interface translations and no unneeded modes; the binary grew from 26 to 31 MB. It loads lazily; if loading fails, a plain field remains. It is vendored into the repository rather than pulled from npm or a CDN: the panel must install on a server without internet access, and the repository has no node tooling.font-src data:was added to the CSP — the icon font is embedded in Monaco's CSS. - API: a
touchoperation (fsop touchwithO_EXCL) — huma does not accept an empty PUT body, and creating a file must not overwrite an existing one;forceempties the file. Tests ininternal/cli/fsop_test.gofor touch and for ".." stopping at the home directory.
Updating the panel from releases (2026-09-06)
-
internal/updater: GitHub releases (or GitHub Enterprise viaapi), chosen by version rather than by publication date; stable/beta channel; fixed artefact names (monopanel_<v>_<arch>.deb,monopanel-<v>.<arch>.rpm,monopanel-linux-<arch>,SHA256SUMS,SHA256SUMS.sig). - Release signature with ed25519:
scripts/release keygen|sign|verify, the private key is a repository secret, the public one isupdate.public_keyinconfig.yaml(root-only, cannot be changed from the panel). With a key set, an unsigned release is not installed; the agent verifies the signature again rather than trusting the hash from the API process. - Installing outside the panel: the agent's
POST /v1/panel/installstarts the transient unitmonopanel-update.service(StartTransientUnit), which survives the restart of the API and the agent;mp update-runinstalls the package, restarts the units, waits for/healthwith the new version and restores the previous binary if it does not answer. The outcome is written to<data>/updates/state.jsonand goes into the audit log after the restart. - API
GET|PUT /system/update,POST /system/update/check|apply(administrator only), thepanel.updatejob, a scheduled daily check andauto_apply; the repository token is encrypted with secretbox. -
mp update,mp update check|apply|settings|trust; the "Panel update" card in the web UI's Settings waits for the restart and reloads the page; anupdatecheck inmp doctor. - Installing the latest version counts as a check in itself (2026-09-24): the
panel.updatejob remembers the release it found, and aftermp update applythe status no longer reports the release seen by the previous check as the latest. - Packages:
make packages(deb+rpm × amd64+arm64 + binaries + SHA256SUMS),make release VERSION=…, therelease.ymlworkflow on av*tag;preremoveno longer stops the panel during an update,postinstallrestarts the units only on an upgrade.
Added while migrating sites from FASTPANEL (2026-09-05)
- Per-site IP allow-list (
sites.allow_from,mp site add|set --allow), the ACME challenge stays reachable. - Importing ready-made certificates (
POST /certificates/import,mp ssl import); Let's Encrypt certificates keep renewing through ACME. - Trusted proxies for
real_ip(mp stack real-ip --cloudflare,--from), the filehttp.d/10-real-ip.conf. - User app services (
apps,mp app): a systemd unit running as the user,ProtectSystem=full,PrivateTmp. - HSTS in the server block and in the static location when HTTPS is forced.
- Web UI: allow-list in the site settings, certificate import, app services and cron in the user card, real-ip in Settings.
- User removal (
DELETE /users/{login}?purge=, jobuser.delete,mp user rm): sites → certificates → databases → app services → crontab → unix account (agentuser/remove: kills the uid's processes,userdel [-r]only for a home under www_root, uid ≥ 1000). - Editing a site's nginx directives (
sites/<domain>.d/custom.conf,GET|PUT /sites/{domain}/nginx,mp site nginx):nginx -t, rollback and the error text in the response; the generated server block is shown read-only. - A site's PHP settings:
GET /sites/{domain}/php(effective values + allowed keys), an overrides editor in the PHP tab; the key list is extended (opcache.jit, session.cookie_*, max_file_uploads …). - CMS presets (
sites.preset,templates/nginx/presets/*.conf.tmplas{{ define "preset-…" }}in the shared template set,presetIniinops_presets.go): WordPress, Joomla, 1C-Bitrix, OpenCart; chosen in the creation form and in the site settings,GET /sites/presets; verified on nginx with real requests (pretty URLs, deny rules, /api, urlrewrite, route). - Web UI design 0.4: light/dark theme tokens (
prefers-color-scheme+data-themefrom localStorage, initialised before the first paint, with a hash in the CSP), animations (page transitions, row stagger, job progress, skeleton), icons, modal confirmations.
Mail server (2026-09-07)
-
mp mail install: postfix + dovecot + opendkim in one job — packages, thevmailuser, configuration, certificate, firewall ports. Verified on Ubuntu 24.04: submission with a real certificate, SASL through dovecot, LMTP delivery, IMAP/POP3/ManageSieve, DKIM signing, rejection of sender spoofing. - Domains, mailboxes and aliases (including catch-all) in the panel database; postfix reads
hash:maps, dovecot a passwd file with{BLF-CRYPT}anduserdb_quota_rule. A disabled mailbox disappears from both maps: mail is not accepted, logging in is impossible, the messages on disk stay. - DKIM: an RSA 2048 key is generated by the panel, the private key is stored encrypted in the database and as a file for opendkim;
mp mail domain dnsshows MX/SPF/DKIM/DMARC/PTR and checks them against public resolvers (1.1.1.1, 8.8.8.8). - The mail certificate comes from the same store as the sites' certificates; on issue and renewal the
cert.issuejob restarts postfix and dovecot itself. While there is no certificate, a self-signed one is put in place, and the panel warns about it. - A busy port 25 (on the dev host it is held by the mail-tester receiver) does not break the installation: the panel identifies the foreign daemon by its banner, turns off receiving from outside and says so. After the configuration is applied, the ports are actually probed — the postfix unit reports "active" even when the master did not come up.
- Webmail:
mp mail webmail <domain>installs Roundcube 1.7.4 (an archive with a sha256 check) as a regular panel site — its own database, its own php-fpm pool, its own certificate;installeris removed. - Web UI
/mail: status, ports, domains with DNS hints, mailboxes, aliases, webmail installation; CLImp mail; tests for configuration rendering, maps, catch-all and yielding port 25. - Webmail on a port:
--port 2096publishes Roundcube on the mail server's name and certificate (its own nginx server block inhttp.d, the port in the firewall, listens on loopback too) — without a separate DNS record or a second certificate. - Lenient domain (
--lenient): the strict HELO and sender checks moved into one list with the recipient checks, and thecheck_recipient_accessmap cuts that list short withOK. This is how postfix took over port 25 on the dev host, while mail-tester fetches mail from a service mailbox over IMAP and takes the sender IP fromReceived— verified with a real message from an external address and a non-existent sender domain (the tester's report: "IP отправителя 83.97.77.254 (по заголовкам)", i.e. sender IP 83.97.77.254, taken from the headers). - The mail page took 4.3 s to load: eight ports were probed one after another, and twice, and on the TLS-wrapped ports the probe waited for a banner that never comes. Probing all at once, once per request → 0.02 s; the web UI fetches the four lists in parallel and shows a skeleton.
- dovecot 2.4 (Debian 13, Ubuntu 26.04) — a different configuration syntax; the installation refuses until the template is written and verified.
- EL 9/10: the packages exist (opendkim from EPEL), the configuration has not been verified.
- Anti-spam beyond the postfix checks (rspamd), changing a mailbox password from Roundcube, mail client autoconfiguration (autoconfig/autodiscover).
Migration between panels (2026-09-09)
- Direct migration MonoPanel → MonoPanel:
mp migrate granton the source,mp migrate plan(a dry run listing conflicts, changes nothing) andmp migrate runon the target. The design — 07-migration.md. - The source is only read: state, a file stream (
tar) and a dump stream. A token with the scopemigrate:user:<login>lets through only GET under/migrateand only for its own account; the other scopes remain labels so as not to break tokens issued earlier. - Passwords move as hashes: the panel's argon2id, yescrypt from shadow (
chpasswd -e),SHOW CREATE USER ... AS '<hash>'for MySQL,{BLF-CRYPT}for mailboxes. Users do not notice the move, and the panel never sees plain text anywhere. - The streams go straight through two new agent operations (
/v1/stream/out,/v1/stream/in) and a recursivechown: the home directory is never stored whole on disk anywhere. - A test with two panels at once: a source fixture and a target fixture with real HTTP between them — it checks the token scope, the dry run, moving an account, a site and cron, the password hash and both tar streams. On a live server the sending half was verified: the file stream, the state with secrets and a certificate.
- A resync (
resync) and a finish (finish) that orders certificates after the DNS switch. - Adapters for foreign panels (2026-09-12):
--from bitrixvmand--from fastpanelread the old server over ssh and produce the same bundle; verified on the testbed against bitrix-env 9 and FASTPANEL 1.11 (scripts/testbed/sources.sh). - The
site:andserver:scopes, a package file, theplainadapter.
Web UI in two languages (2026-09-13)
- The whole web UI is translated into English; Russian and English live in the dictionary
web/src/lib/i18n/msgs/*.ts(one fragment per page),t()/tn()take the current language from the state — switching redraws the interface without a reload. - The default language comes from the browser language: Russian for the languages of CIS countries (ru, uk, be, kk, ky, uz, tg, tk, hy, az, ka, ro-MD…), English for everyone else; the same list is in
app.html, so that<html lang>is correct before the app starts. - The "Auto / Русский / English" switch in Settings and on the sign-in screen; the choice is kept in
localStorage.lang; dates and units (when(),bytes()) follow the language. - API messages, job logs and doctor are in English (2026-09-25); the placeholder page of a new site and the page of a suspended one follow the visitor's browser language, and the interface translates the presets and the CMS catalogue itself.
- The CLI and the TUI are Russian-only for now; Russian API messages for the Russian interface need a dictionary on the Go side and a language on the job.
Firewall: rule order and lockout protection (2026-09-14)
- The chain is assembled in tiers: allow with a source → deny → always-open ports and allow without a source. Previously deny came first, and "deny 8443 + allow 8443 from the VPN" closed the panel to everyone, the VPN included.
- Guard: a deny without a source on SSH or the panel port is accepted only when an allow with a source exists; removing the last such allow, as well as a deny or ban that covers the request's address, is refused. The status shows the closed ports and whom they are open to (
restricted, a line inmp firewall status, tags on the page).
Static files: compression and caching (2026-09-15)
- The editor was slow to open: http.FileServer served the embedded static files without gzip, ETag or Cache-Control, and every page reload fetched the 4.4 MB of Monaco again in six sequential requests. Now a file is gzipped once into memory (Monaco 4.4 → 1.2 MB), hashed chunks (
_app/immutable/*,name-<hash>.js) are cached for a year as immutable, everything else isno-cachewith a weak ETag and 304. FileManager preloads Monaco during idle time as soon as the files page opens, not on a click.
API documentation without a third-party origin (2026-09-21)
- The
/api/v1/docspage loaded Stoplight Elements from unpkg.com, and the panel sets no CSP for/api/paths: a third-party script ran on the panel's origin next to the administrator's session. Now Elements 9.0.15 is embedded in the binary (internal/api/elements, Apache-2.0), and the page and its files are served by the panel itself with the CSPdefault-src 'none'; script-src 'self', without inline scripts or eval; a test pins the files' sha384 to the npm release and checks that the page has no third-party addresses. - The documentation, the OpenAPI specification (
/openapi*) and the JSON schemas (/schemas/*) are behind authentication: a session or an API token of any account; a migration token gets 403, an anonymous visitor 401 (a browser following a link to/docsis redirected to sign-in). Only/healthand/auth/loginremain available without signing in. -
/healthreturns the panel version only to a signed-in caller (session, token, local socket); an anonymous one getsstatusandtime. The sign-in screen no longer shows the version.
Global PHP settings (2026-09-23)
- php.ini values had a "panel" source that could only be changed one site at a time. Now there are layers: panel → global → preset → site. The global layer is
GET|PUT /php/settings(admin),mp php ini [set k=v|unset k]and the "PHP settings for all sites" card on the PHP page; saving rebuilds the pools of all PHP sites (site.applyfor each). A site's PHP tab shows the "global" source and the order of the layers.
Site log rotation (2026-09-24)
- Nobody rotated the logs in
/var/www/<login>/data/logs: the panel wrote no logrotate configuration, and the distribution's configs do not cover that path. Now/etc/logrotate.d/monopanel-siteshas a block per account withsu <login>(the directory belongs to the client, root does nothing in it): weekly or at 100 MB, eight copies, compression from the second one,create 0660 <login> <login>. All of a site's logs belong to the account with mode 0660: the panel creates them before nginx, Apache and php-fpm open them, and hands over to the account the ones those daemons created earlier — via a newmodeflag of the agent'schownoperation, through an open file (O_NOFOLLOW,fchown,fchmod). Otherwise logrotate 3.18 on EL9 could not cope: when rotating it opens the log, and when compressing it opens it for writing. The file is rewritten when an account is created or removed (the removed account's entry goes beforeuserdel, so that logrotate does not meet a non-existent user) and when the panel starts; before writing, it is checked withlogrotate --debug. - nginx worker processes could not reopen the site logs after USR1 — neither after this rotation nor after nginx's own nightly rotation:
data/logshad no traverse permission for themonopanel-webgroup, so every nightemerg … Permission deniedlanded in the nginx log and writing continued into the old file.data/logsgot the ACLg:monopanel-web:xon site creation and onmp site fix, and existing accounts got it at panel start. Apache reopens its logs with a graceful restart on USR1 via the pid file:systemctlcalled from logrotate runs into SELinux on EL. - The php-fpm slow log on EL was empty: php-fpm takes the trace via
ptraceof the worker process, and the stock policy silently denieshttpd_tthesys_ptracecapability. The panel installs its own policy modulemonopanel(CIL,semodule -i) withallow httpd_t self:capability sys_ptraceandself:process ptrace— together with preparing the host for hosting, and on already prepared hosts at startup, without another relabel. - Verified on Debian 13 (with Apache), Ubuntu 22.04 (logrotate 3.19), Rocky 9 (3.18) and AlmaLinux 10 in enforcing: after rotation nginx, Apache, PHP and php-fpm write to the new files, the second rotation compresses the first as the account, the slow log on EL has traces,
logrotate.servicesucceeds, no SELinux denials.
Valkey per account (2026-09-24)
-
mp stack install valkeyinstalls Valkey from the distribution (Debian 13, Ubuntu 24.04/26.04, EL9/EL10 AppStream, Debian 12 — from backports, if they are enabled), and where it has none — Ubuntu 22.04, Debian 12 without backports — Redis (6.0 and 7.0) with the same protocol; the package's shared instance (127.0.0.1:6379 without a password, for everyone) is stopped and disabled. The server cannot be removed while there are instances on it. - An account has two separate instances:
cache(allkeys-lru,--save "") andsessions(volatile-lru, a snapshot every minute). Each one ismonopanel-valkey-<login>-<purpose>.servicerunning as the user,Type=notify(a restart returns once the snapshot is loaded and the socket accepts connections),MemoryMaxat twice maxmemory for headroom, the whole configuration in the arguments (the account has no access to/etc/monopanel). It listens only on a unix socket with mode 600 in a 0700 directory (RuntimeDirectory=), snapshots go toStateDirectory=;mp valkey add|list|restart|rm,/users/{login}/valkey[/{purpose}], the valkey button on the "Users" page. Removing an account takes its instances with it; migration between panels carries over their settings without the data. - A site's PHP sessions in Valkey:
mp site set <domain> --sessions valkey|files, the "PHP sessions" field in the site settings; the pool getssession.save_handler = redisand the pathunix://…/valkey.sockthrough php_admin_value. This needs the sessions instance and the redis extension on the site's PHP branch; while a site keeps its sessions there, neither the instance nor the extension can be removed (409); a site in proxy mode keeps no sessions. - SELinux (EL): the sockets are
redis_var_run_t, the snapshot directoriesredis_var_lib_t, while/var/lib/monopanel-valkeyitself staysvar_lib_t: init_t cannot add entries to aredis_var_lib_tdirectory, andStateDirectory=failed with EACCES without a single AVC. The unit has noProtectKernelTunables=,ProtectKernelModules=orRestrictAddressFamilies=: for a non-rootUser=they turn on NoNewPrivileges, and under it the EL9 policy does not allow the init_t → redis_t transition, so the server stayed in init_t without the right to write snapshots. Live check on Debian 13, Ubuntu 22.04, AlmaLinux 10 and Rocky 9 (enforcing): another account and the web server get "Permission denied", a PHP session through php-fpm survives a restart, no AVCs.
Stage 3 — v1.x
-
proxymode for Node/Python/Docker applications. - Mail: postfix + dovecot + opendkim + Roundcube (Debian/Ubuntu).
- Migrating accounts between panels (MonoPanel → MonoPanel).
- Isolated pools with cgroup limits, FTP, DNS (PowerDNS), WAF, a reseller role, an aarch64 build (cross-compilation is ready:
make build-arm64), multi-server.
OS matrix
| OS | Modes | PHP (sample) | Database |
|---|---|---|---|
| Debian 12, 13 | A, B | 5.6, 7.4, 8.2, 8.5 | Percona 8.4, MySQL 8.4 |
| Ubuntu 22.04, 24.04, 26.04 | A, B | 7.4, 8.3, 8.5 | Percona 8.4 |
| AlmaLinux, Rocky Linux, Oracle Linux 9, 10 | A, B | 7.4, 8.4, 8.5 (+ 5.6 on EL9) | Percona 8.4, MySQL 8.4 |
Runner: the testbed on Proxmox — eleven VMs from cloud images with cloud-init and a rollback to a clean snapshot; it is run from a workstation, not in CI. Containers are not enough for SELinux, nftables, quotas and systemd slices.
The make testbed-matrix scenario: package installation → mp setup → nginx, PHP, database server → e2e (user → site with a preset → database → nginx and PHP answer → removal) → mp doctor without errors. Before a release, make testbed-full adds migrations between panels, CMSes on every machine and moves from BitrixVM and FASTPANEL. Planned: HTTPS via Pebble, backup → restore → suspend → purge.
A periodic job (weekly) is planned: package availability in the vendor repositories across the matrix and the appearance of new versions (PHP 8.6/9.0, nginx stable, Percona 8.4.x, OS releases).
Risks and how they are addressed
| Risk | Mitigation |
|---|---|
| Sury/Remi disappear or break compatibility | Stage 2: own builds and repository; OS Profile allows keeping both sources at once |
MySQL 9.x removes mysql_native_password | We stay on 8.4 LTS until 2032; for legacy PHP — 8.4 only; upgrading to 9.x is a manual procedure |
| A new OS release without vendor packages | Support is announced only after the matrix passes; a weekly check |
| Manual config edits by the administrator | Include directories, confhistory, mp doctor shows drift, mp site apply regenerates the site |
| Compromise of the web layer | api without privileges; agent with an allow-list; helper with setuid; secrets are encrypted |
| Growth in the number of sites (hundreds) | pm=ondemand, one master per version, SQLite with WAL holds tens of thousands of entities; metrics are rollups |