Repository navigation
Releases: paskal/bitrix.infra
Release list
Version 4.1.0
Image optimisation works again where a locale mismatch stopped it, PHP images pick up package security updates on every publishing build, PHP-FPM gains request draining for site releases, and the Zabbix templates gain certificate, domain, RAID and 1C checks. Yandex Cloud DNS users must re-enable the token refresh after upgrading (upgrade note 2).
Image optimisation
- A locale mismatch could stop every run before it processed a file: the file lists were sorted in the C locale, but
commran in the cron locale and rejected them, and cron discarded the output. The script now runs in the C locale, and cron appends its output tologs/optimise-images.log. - WebP images are no longer re-encoded.
cwebp -q 100re-encodes them lossily and often makes them larger. PNG, JPEG and GIF optimisation stays lossless. - A file whose path and size match its database row counts as optimised even when its modification time changed, for example after a copy to a new host or the monthly hard-link deduplication.
- Progress is saved every 500 files, a lock stops the next weekly start while a long run is still going, and files changed in the last ten minutes wait for the next run.
--originals-onlyleaves outupload/resize_cache(Bitrix thumbnails) and keeps their rows, for working through a backlog of originals first.
PHP
- PHP-FPM gains request-draining support for
/web/prodand/web/dev, controlled by an external release script:config/php/deploy-gate.phpis prepended to every php-fpm request (not to CLI or cron). Requests wait up to 15 seconds while the gate is locked, then continue without tracking. Without the lock files underlogs/deploy-gate/the gate is inactive. - The PHP 8.3, 8.4 and 8.5 images run
apt-get upgradein a layer that is rebuilt on every publishing build, so PHP security releases from the package repositories land without waiting for a new base image. The image workflow can also be started by hand.
Disaster recovery and scripts
- Disaster recovery installs a
docker.servicedrop-in that re-applies the Zabbix agent's ACL on the Docker socket whenever dockerd starts. dockerd recreates the socket on every restart, an unattended package upgrade included, which left Docker monitoring blind until the next reboot. - Newly created
zabbix,memcachedandmysqlservice accounts use/usr/sbin/nologininstead of theuseradddefault/bin/sh. seo-reindex.shexits non-zero when it attempted URLs and every submission was rejected, so cron-based monitoring sees the failure. Runs without quota, with an empty queue or with partial success still exit 0.renew-dev.shexcludesupload/acrit.export/from the copy; existing dev copies of those files remain.- The Yandex Cloud DNS token refresh is no longer scheduled: its line in
config/cron/host.cronis commented out. Providers with a static API token, such as DigitalOcean, need nothing there.
Zabbix
- Certificate triggers lost in an earlier template reshuffle are back, and the expiry item is a float so negative days after expiry stay valid. Domain registration expiry is checked through the server-side
ssl_cert_check.shdomain mode. - A new template monitors Linux MD RAID state from
/proc/mdstat. - The tg-agent template alerts when the end-to-end MySQL tunnel check fails three times in a row or stops reporting for an hour, and the ads-cron failure alert names the cause from the last line of the run output. Both rely on the tg-agent side pushing those values.
- A new 1C template reads a daily check for changes made outside releases and a deferred-queue check from site endpoints. Its defaults point at favor-group.ru and need tokens, so import it only if you run those endpoints.
Upgrade notes
- With your overlay's
COMPOSE_PROFILESset:docker compose pull php php-cron && docker compose up -d php php-cron. nginx keeps the old php address until it reloads, so test and reload it afterwards (docker compose exec nginx nginx -t && docker compose exec nginx nginx -s reload). - If you renew certificates through Yandex Cloud DNS, uncomment the
update-dns-token.shline inconfig/cron/host.cron; otherwise the token expires within 12 hours and renewal fails. - On existing hosts, install the
docker.servicedrop-in fromzabbix_docker_acl_dropininscripts/disaster-recovery.shby hand and runsystemctl daemon-reload. - Re-import the templates you already use (site monitoring, www redirect, tg-agent monitoring). The MD RAID template is new: import it and link it to the hosts with software RAID.
- If earlier image optimisation runs failed, the first run after the upgrade may process a large backlog. On a large site start it by hand with
--originals-only, then let the weekly run take the thumbnails.
Version 4.0.0
php and php-cron move to PHP 8.5, with sessions kept in memcached through PHP's native handler. Disaster recovery can restore from another hostname and S3 endpoint, rebuild nested Git checkouts and run site-specific steps, backups can go to a second S3 destination, and the deploy account is configurable. Read the upgrade notes before pulling.
Why a major version
docker-compose.yml now runs php and php-cron on the bitrix-php:8.5 image with their config binds under /etc/php/8.5. That image does not ship the memcache extension, so a Bitrix site whose bitrix/.settings.php uses the memcache session type fails every request after the switch. Bitrix has no memcached session type; the save_handler.php.ini type hands sessions to PHP's native handler, which config/php/90-php.ini now points at memcached-sessions:11211. Change the session setting before pulling (upgrade note 1).
PHP and sessions
- Sessions use the php-memcached native handler. A request waits up to about 30 seconds for a session lock held by another request of the same visitor, and a lock expires after 60 seconds.
- nginx resolves the php upstream once, so after php is recreated it keeps the old container address and answers 502 until it reloads; the PHP switching steps in the Readme say so.
MySQL and memcached
sort_buffer_sizeincreases from 256K to 2M to reduce disk-based sorts. MySQL grows sort memory as needed, so this is a per-connection cap, not a reservation.- The MySQL container sets
oom_score_adj: -500to make it less likely to be selected during an out-of-memory event. - The component cache memcached gets 3 GB instead of 2 GB.
- Sizing: the 4 GB InnoDB buffer pool (unchanged since v3.0.0) needs a host with at least 8 GB of RAM; on a 4 GB host the kernel kills mysqld while it initialises the data directory. The Readme says how to shrink it locally.
nginx
- The open file cache is off. It kept a replaced file's descriptor and size for up to 30 seconds, so a new versioned URL could receive the old bytes, or the new content cut to the old size, and browsers could keep that response for the configured cache lifetime.
- In vhosts using
bitrix.conf, the root icon paths (/favicon.ico,/favicon.svg,/apple-touch-icon.png) are cached for a day instead of as immutable, and feed.xmland.txtfiles under/upload/are revalidated within the hour. - The static-host CORS origin is an overlay map in
conf.d/overlay-maps.conf, so includingstatic-cdn.confwithout a private overlay no longer fails the configuration test.
Disaster recovery
RESTORE_FROM_HOSTselects the hostname whose backups are restored, andRESTORE_S3_ENDPOINTandRESTORE_AWS_PROFILEselect the S3 endpoint and the credentials profile. The bucket, domain and database name remain site-specific values at the top of the script. Without the variables the script restores from the default hostname and endpoint as before; the credentials now come from the deploy account's~/.aws.- After the restore merge, the repository's tracked files are put back to the clone, since the backup's copies can be older.
DR_NESTED_REPOSlists restored checkouts, such as the private overlay, as<dir>=<git url>pairs; the script turns each into a Git repository trackingorigin/masterthat keeps the restored working files, using the restoredprivate/updater_ssh_keyand a pinned GitHub host key, because the backups leave out.git. - An executable
private/scripts/dr-hook.shis called withpre-startbefore the containers start and withpost-dbafter the database import, for site-specific steps. - The script installs the host tools the backup jobs need, sets up the deploy account (uid 1000, passwordless sudo, Docker group, the updater's SSH key) and works without a terminal; the Readme section "Disaster recovery" has the details.
- The database import waits until the final mysqld is running and accepts the root login. On a fresh host the image's temporary initialisation server already answered
mysqladmin ping, so the import started too early and failed withERROR 1045. - The MTU settings for Yandex Cloud DDoS protection apply only on Yandex Cloud; on other hosts the network is left alone.
Backups
- When
BACKUP2_S3_BUCKETis set inbackup.env,file-backup.shalso runs its own duplicity chain against that bucket andmysql-dump.shsyncs the dumps there. A failure at one destination does not stop the other; the run still exits non-zero and leaves the monitoring status file unwritten.
Deploy account
scripts/deploy-user.shresolves the account that owns the checkouts, runs the deploy pulls and keeps the backup credentials in~/.aws, fromDEPLOY_USERin the environment or in.env, withadminas the default. The recovery, permission, pull and backup scripts use it, and the DNS token cron job runs as it throughrunuser.- When that account has uid 1000, the nightly
fix-rights.shalso gives the tracked files, every directory recorded inHEADand.gitof the public and private checkouts to it, setsprivate/environmentsecrets to mode 0600 and the Zabbix agent's MySQL credentials to uid 1997 with mode 0400. Cron and logrotate files stay root-owned and MySQL configuration stays uid 1001. The repair is skipped when the account does not have uid 1000.
Dev renewal
renew-dev.shreads its site-specific values from the environment, with the previous values as defaults, and refuses destinations that overlap production. It now needs GNUrealpath -m, refuses a productiondbconn.phpwhose$DBLoginor$DBNameis not a single quoted literal assignment, and no longer rewrites theskypark.cdnmodule'scdn_domains1; provide that throughPOST_RENEW_HOOKif you need it.
Other changes
update-dns-token.shleavesAUTH_KEYalone when the DNS provider is not Yandex Cloud.- Upload deduplication skips hidden
.*-backupsrollback directories. - The PHPStan triggers carry a
notifytag from the template macro{$PHPSTAN.NOTIFY}, empty by default, so alert actions can route them. Re-import the template and set the macro to the value your actions match.
Upgrade notes
- Before pulling, change the general session handler in the
sessionblock ofbitrix/.settings.phpto'general' => array ('type' => 'save_handler.php.ini')(the Readme has the full example). Move any local override that binds files under/etc/php/8.4/to/etc/php/8.5/. After the pull, with your overlay'sCOMPOSE_PROFILESset:docker compose pull php php-cron && docker compose up -d php php-cron, then test nginx and reload it (docker compose exec nginx nginx -t && docker compose exec nginx nginx -s reload). docker compose up -d --timeout 120 mysqlrecreates mysqld with the newmy.cnfandoom_score_adj; it interrupts the database.- Recreate memcached with
docker compose up -d memcachedfor the 3 GB limit, and check the host has the memory. - nginx reads
nginx.confthrough a file-level bind mount: test the new file in a fresh container as the Readme describes, then restart nginx. - If the host has an account named
adminwith uid 1000, the nextfix-rights.shrun changes the ownership of both checkouts and ofprivate/environmentas described above. If your deploy account has another name, setDEPLOY_USERin.envbefore that run, and make sure that account holds the backup credentials in its~/.awsand, for Yandex Cloud DNS renewal, theycCLI in its home.
Version 3.0.0
MySQL defaults change to utf8mb4 and an in-place converter with verification is included for existing databases. Existing table definitions and Bitrix connection settings are not changed automatically. Read the upgrade notes before pulling.
Why a major version
config/mysql/my.cnf now sets character_set_server=utf8mb4, collation_server=utf8mb4_unicode_ci and default_character_set = utf8mb4 for the client. These are defaults: databases created without an explicit charset get utf8mb4 once the container reads the new file, and the mysql client and mysqldump read data as utf8mb4. The backup, restore and dev renewal scripts ask for utf8mb4 themselves, so they change right after the pull, independently of the restart or of any table conversion. Storing 4-byte characters (emoji, some CJK and mathematical symbols) still needs the conversion below.
Database conversion
scripts/convert-utf8mb4.sh DATABASEconverts a database in place with oneALTER TABLEper table that needs it, generated frominformation_schema. Each character column outside both the selected collation andutf8mb4_binis rewritten with its own type, nullability, default and comment;*_bincolumns becomeutf8mb4_bin, all othersutf8mb4_unicode_ci(or--collation utf8mb4_0900_ai_ci, the MySQL 8 default that fresh Bitrix installs use). A plainCONVERT TO CHARACTER SETwould widenTEXTtoMEDIUMTEXTand turn binary collations into the table collation, which breaks unique keys such asb_search_stem.STEM.- Before and after the run the script writes a column snapshot and a per-table fingerprint (row count and folded MD5 of every row) under
logs/utf8mb4/and exits non-zero when a definition changed beyond charset and collation or a fingerprint differs. A table the application wrote to during the run shows up as a fingerprint difference too, so stop everything that writes first. - The run stops before changing anything on: generated or invisible columns that still need conversion, foreign keys on character columns,
VARCHARlonger than 16383 characters, line breaks in a default, type or comment, backticks or control characters in table or column names, views, non-InnoDB tables, andNO_BACKSLASH_ESCAPESin the globalsql_mode. --dry-runwrites the statements without executing them. A run that stops halfway can be repeated; the retry does not check against the failed run's snapshots, so inspect that run's diff files before retrying.
Bitrix settings
The Readme section "Converting the database to utf8mb4" lists the connection settings that go with the conversion: 'charset' => 'utf8mb4' and 'utf8mb4' => array('global' => true) in bitrix/.settings.php, and bitrix/php_interface/after_connect_d7.php with SET NAMES 'utf8mb4' COLLATE '...' naming the collation the tables were converted to; Bitrix includes that file on every connection by default and its install wizard creates it on fresh installs.
Dev renewal
scripts/renew-dev.shpublishes the dev DB connection files only after the dump is restored; until then the dev site has no database access. Before, requests during the restore hit an empty database and Bitrix cached the empty event handler list, an entry with no expiry, so dev answered 500 until that cache was cleaned by hand. The generated files wait in a root-only temporary directory and the dev tree never holds production credentials.- Open sessions of the dev user are closed on a best-effort basis before the database is recreated (
DROP USERleaves them alive, and php-fpm keeps persistent connections), the dev database takes the charset and collation of the production one, and after the file sync the script cleans the dev managed and file caches through the Bitrix API and removes the dev host's composite page directory. When prod and dev share a cache backend, dev needs its ownsidin.settings_extra.php, otherwise this cleanup also hits production entries.
Upgrade notes
config/mysql/my.cnfis a file-level bind mount, so the container keeps reading the old inode after the pull.docker restart --timeout 120 mysqlmakes it read the new file; that is a database interruption, and the timeout gives mysqld time to shut down cleanly instead of the 10-second default.- To convert an existing database: take a backup, stop the site and
php-cron, runsudo ./scripts/convert-utf8mb4.sh --dry-run DATABASEand then without--dry-run, add the Bitrix settings from the Readme, reload php-fpm, startphp-cronand reopen the site. The released script converted a 1.35 GB Bitrix database of 744 tables in about three minutes ofALTER; the window also holds the two fingerprint passes and the application checks, so rehearse on a restored copy to size it. - Existing databases are not converted automatically. New databases created without an explicit charset or collation use the new server defaults;
renew-dev.shcreates dev with the production database's charset and collation.
Version 2.2.0
Operations release: every core container now reports a health state, uploaded SVG is sandboxed, identical files under upload/ can be merged monthly, and the production pull wrapper validates nginx configuration on the right network.
Health checks
nginx,php,php-cronandmysqlcarry health checks indocker-compose.yml: nginx probes itsstub_statusvhost on port 8084, php opens a TCP connection to port 9000 (replacing the image'sphp --ini, which passed while php-fpm was down), php-cron looks for the cron daemon, mysql pings over its socket. Start order is unchanged; dependencies do not wait on health checks.- On hosts that already use the standard "Docker by Zabbix agent 2" template, it can alert on the new health states.
nginx
/upload/**.svgis served withContent-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox, the policy bitrix-env ships for the same path, so script inside an uploaded SVG cannot run in the site origin when the file is opened as a document. The same policy disables other active or external content in such documents; a site that serves interactive SVG documents fromupload/needs to adjust the rule.- The scanner fast-404 list gains
.old,.saveand a trailing~.
Upload deduplication
scripts/dedup-upload.shreplaces byte-identical files underweb/prod/uploadwith hard links usinghardlinkfrom util-linux: owner, mode and extended attributes must match, only the modification time is ignored. The listed scratch, exchange and log directories of known modules are excluded, a lock keeps two dedup runs apart, and the fullhardlinkoutput with its exit status goes tologs/dedup-upload.log;--dry-runshows what a run would save.config/cron/host.cronruns it on the first of every month at 02:20 host time.
Fixes
scripts/pull-public.sh, this deployment's pull wrapper, validates a changed nginx file bind in a fresh container on php's network, so the php upstream resolves when nginx belongs to more than one network. Before, the test container joined the first network in nginx's list, the validation failed there and nginx was not restarted.
Upgrade notes
- Recreate
nginx,php,php-cronandmysqlafter pulling so the health checks attach; the mysql recreate restarts the database. - The deduplication needs
hardlinkfrom util-linux andflockon the host; the script exits with a message when either is missing. - Hard links share later in-place writes between the merged copies. Before enabling the monthly job, check which processes write under your
upload/and extend the excluded directories in the script accordingly; the lock only prevents a second dedup run, it does not hold off importers or the image optimiser.
Version 2.1.0
A hardening release on top of the 2.0 engine: nginx denies Bitrix routes the site does not use, the backup and restore scripts fail instead of reporting success on a partial dump, the dev-site renewal no longer publishes production connection settings, and a Zabbix template for the backups is supplied. The upgrade notes below list the deployment changes an existing installation has to make.
Security
- nginx mounts both web trees read-only. PHP keeps its writable mounts for uploads, cache and updates; a compromised nginx worker can no longer plant a script the PHP location would execute.
- Server-side source files under
/upload/(.php,.phtml,.pht,.phar,.inc) return 404, and uploaded HTML is served as plain text, as the Bitrix advisory for CVE-2023-1720 recommends. - Denied Bitrix routes:
/bitrix/tools/bizproc/user_selector.php,/bitrix/tools/calendar/push.php,/bitrix/tools/stssync.php, the twosonet_*tool loaders, the untracked Bitrix24 helper proxy at/ajax/b24_bp_handler.php, and the SProduction basket-sync endpoint. A fork that uses one of these modules needs to lift the correspondingreturn 404inbitrix.conf. The SProduction deal callback remains reachable, and its diagnostic logs under/upload/sproduction.integration/are no longer public. - Security headers are emitted on error responses as well.
bots.confdenies PHP to crawlers that ignorerobots.txtand to rotating scanner ranges; static assets are still served. The README documents which user agents are flagged and which are denied.- Adminer runs on its own internal network with nginx only. It reaches MySQL over the mounted socket and no longer needs the shared bridge, where a user-supplied server field could make it open connections to any container.
- Zabbix MySQL credentials moved out of item keys into a mounted client configuration (see upgrade note 1).
renew-dev.shbuilds the dev copies ofdbconn.phpand.settings.phpfrom the production files before the copy and excludes both from rsync; a connection file in a layout it cannot rewrite aborts before the dev database is dropped, so production connection settings are never published to the dev site.
Backups and recovery
mysql-dump.shruns under bash withpipefailand promotes the dump from a temporary name only after it passed the checks, so a failed or undersized dump is never uploaded, while a valid dump survives a failed S3 sync. Any compressed archive below 1,000,000 bytes is rejected; a fork with a small database has to lower that guard in the script. Credentials and temporary output are removed when the script fails, and the S3 sync skips leftover temporary dumps.disaster-recovery.sh: the unattended restore used to skip itself on a fresh clone; it now restores into a staging directory, waits for MySQL and for the unattended-upgrades dpkg lock, checks the archive and runs the import underpipefail.compare-backups.shfails on a diff error instead of reporting a completed comparison, and restores into fresh directories.config/zabbix/templates/backup-monitoring.yamlalerts on stale or empty backups once imported and linked to the host.
nginx
- Bitrix composite snapshots are served directly by nginx,
HEADincluded. This is inert until composite is enabled in the Bitrix admin and snapshots exist; see "Composite site" in the README.
Operations
scripts/pull-public.shis the production pull wrapper for this deployment: it handles the root-owned cron and logrotate directories, restartsphp-crononly when its file-mounted cron file changed inode, and tests a changed nginx file bind in a fresh container before restarting nginx. It is written for this repository's origin and layout; the README paragraph onscripts/pull-public.shcovers adopting it.- logrotate rotates the site cron logs under the webroot, no longer trips over a duplicate glob that made every daily run exit 1, and the MySQL postrotate sources its credentials by absolute path, so
flush-logsruns. - Bitrix cron stderr lands in
cron_events.log. - Dev renewal runs
private/scripts/renew-dev-post.shfrom the overlay when present. - Optional Zabbix templates for the site automation live under
config/zabbix/templates/, with their alert routing inconfig/zabbix/ALERT-ROUTING.md.
Upgrade notes
- Monitoring profile:
private/environment/zabbix-mysql-session.confis now a required bind forzabbix-agent, so the service cannot be recreated without it. Copyconfig/zabbix/mysql-session.conf.examplethere with your monitoring credentials, then in Zabbix set{$MYSQL.DSN}toLocalMysqland leave{$MYSQL.USER}and{$MYSQL.PASSWORD}empty (private/environment/zabbix.env.example). - FTP moved to the
ftp-manualprofile withrestart: no; start it on demand and do not add the profile toCOMPOSE_PROFILES. The updater no longer publishes a host port; nginx reaches it on the Compose network, and anything that connected to the host port has to go through nginx. - Recreate nginx and Adminer after pulling:
COMPOSE_PROFILES=<your profiles> docker compose up -d --force-recreate --no-deps nginx adminer. The read-only web mounts and the Adminer network isolation apply only to recreated containers. Nothing in this repository writes site files through nginx; a workflow that did now fails with "Read-only file system". scripts/mysql-dump.shis a bash script. Run it as an executable, as the shipped cron line does, not assh scripts/mysql-dump.sh.binlog_expire_logs_secondsinconfig/mysql/my.cnfis 432000 (five days); it was 18000, five hours, too short to bridge two dumps. Apply it live withSET GLOBAL binlog_expire_logs_seconds=432000or restart MySQL, and allow disk for five days of your binlog volume.
Version 2.0.0
The public repository is now a generic Bitrix infrastructure engine: a fresh clone boots the official Bitrix demo at http://localhost with no source changes (verified independently by three AI agents following only the README). Site-specific configuration lives in a private overlay repository and re-attaches via docker-compose.override.yml, nginx include globs and map-entry files — the pattern is documented in the README ("Production overlay") and is validated in CI on every change.
Breaking changes — migration guide for forks
- Site vhosts, redirect maps, Metrika cookie pinning, CSP snippets, four Zabbix templates, PHPStan scopes, site cron jobs, the updater task list and
bin/yandex-reviewsleft this repository. If you customised any of them, move your versions into your own overlay following the README pattern. container_name:is gone from compose:docker exec nginx ...becomesdocker exec <project>-nginx-1 ..., or pin names in your owndocker-compose.override.yml.- Values now come from
.env(see.env.example); check the defaults against your deployment:MYSQL_DATABASEdefaults tobitrix(wasfavor_group_ru),DOCKER_MTUto 1500 (was 1450 — Yandex Cloud DDoS-protected IPs need 1450),HTTP_PORT/HTTPS_PORTto 80/443,TZto Europe/Moscow (unchanged). - nginx carries no TLS material: certificate paths moved out of the shared files — declare them per vhost in your overlay (
ssl.confinclude pattern). CSP includes are now globs that tolerate absence; hotlink protection,X-Frame-Optionsand admin CORS are driven by maps extendable viaprivate.conf.d/*.map. FastCGIHTTP_HOSTuses the port-preserving$bitrix_hostmap. - Scripts now read site values from env files: backup scripts need
private/environment/backup.env,seo-reindex.shneedsQUOTA_HOSTin its env,urls.pyneedsSITE_URLand writes the redirect map to the overlay path. - New: localhost demo vhost,
AGENTS.md+llms.txtfor coding agents, and avalidateCI workflow guaranteeing a fresh clone always passesnginx -tand compose config.
Version 1.0.0
First tagged release. This repository is the complete production infrastructure of a real 1C-Bitrix site (favor-group.ru), running it since 2020: a docker-compose stack (nginx with HTTP/3, brotli and Lua; PHP-FPM 8.4; socket-only Percona Server 8.4; separate memcached instances for cache and sessions), Let's Encrypt automation via DNSroboCert, Zabbix monitoring including a weekly PHPStan scan of the deployed code, incremental S3 backups, and the maintenance scripts around all of it.
The stack has been verified end-to-end: a fresh clone of this tag can serve the official Bitrix demo distribution locally after manual preparation steps (self-signed certificates, stub files, port remapping).
Versioning policy from this point on: MAJOR versions change how you deploy or lay out files (compose service or mount renames, nginx config restructuring, environment variable changes); MINOR versions add services, scripts or documentation; PATCH versions fix bugs.
Planned for 2.0.0: extraction of all site-specific configuration into a private overlay, making this repository a generic Bitrix infrastructure template that boots a localhost demo with zero modifications. Release notes will include a migration guide for existing forks.