Skip to content

Releases: paskal/bitrix.infra

Version 4.1.0

Choose a tag to compare

@paskal paskal released this 26 Oct 19:28
e375970

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 comm ran 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 to logs/optimise-images.log.
  • WebP images are no longer re-encoded. cwebp -q 100 re-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-only leaves out upload/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/prod and /web/dev, controlled by an external release script: config/php/deploy-gate.php is 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 under logs/deploy-gate/ the gate is inactive.
  • The PHP 8.3, 8.4 and 8.5 images run apt-get upgrade in 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.service drop-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, memcached and mysql service accounts use /usr/sbin/nologin instead of the useradd default /bin/sh.
  • seo-reindex.sh exits 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.sh excludes upload/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.cron is 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.sh domain 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

  1. With your overlay's COMPOSE_PROFILES set: 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).
  2. If you renew certificates through Yandex Cloud DNS, uncomment the update-dns-token.sh line in config/cron/host.cron; otherwise the token expires within 12 hours and renewal fails.
  3. On existing hosts, install the docker.service drop-in from zabbix_docker_acl_dropin in scripts/disaster-recovery.sh by hand and run systemctl daemon-reload.
  4. 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.
  5. 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

Choose a tag to compare

@paskal paskal released this 14 Oct 04:42
0a45299

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_size increases 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: -500 to 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 .xml and .txt files under /upload/ are revalidated within the hour.
  • The static-host CORS origin is an overlay map in conf.d/overlay-maps.conf, so including static-cdn.conf without a private overlay no longer fails the configuration test.

Disaster recovery

  • RESTORE_FROM_HOST selects the hostname whose backups are restored, and RESTORE_S3_ENDPOINT and RESTORE_AWS_PROFILE select 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_REPOS lists restored checkouts, such as the private overlay, as <dir>=<git url> pairs; the script turns each into a Git repository tracking origin/master that keeps the restored working files, using the restored private/updater_ssh_key and a pinned GitHub host key, because the backups leave out .git.
  • An executable private/scripts/dr-hook.sh is called with pre-start before the containers start and with post-db after 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 with ERROR 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_BUCKET is set in backup.env, file-backup.sh also runs its own duplicity chain against that bucket and mysql-dump.sh syncs 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.sh resolves the account that owns the checkouts, runs the deploy pulls and keeps the backup credentials in ~/.aws, from DEPLOY_USER in the environment or in .env, with admin as the default. The recovery, permission, pull and backup scripts use it, and the DNS token cron job runs as it through runuser.
  • When that account has uid 1000, the nightly fix-rights.sh also gives the tracked files, every directory recorded in HEAD and .git of the public and private checkouts to it, sets private/environment secrets 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.sh reads its site-specific values from the environment, with the previous values as defaults, and refuses destinations that overlap production. It now needs GNU realpath -m, refuses a production dbconn.php whose $DBLogin or $DBName is not a single quoted literal assignment, and no longer rewrites the skypark.cdn module's cdn_domains1; provide that through POST_RENEW_HOOK if you need it.

Other changes

  • update-dns-token.sh leaves AUTH_KEY alone when the DNS provider is not Yandex Cloud.
  • Upload deduplication skips hidden .*-backups rollback directories.
  • The PHPStan triggers carry a notify tag 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

  1. Before pulling, change the general session handler in the session block of bitrix/.settings.php to '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's COMPOSE_PROFILES set: 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).
  2. docker compose up -d --timeout 120 mysql recreates mysqld with the new my.cnf and oom_score_adj; it interrupts the database.
  3. Recreate memcached with docker compose up -d memcached for the 3 GB limit, and check the host has the memory.
  4. nginx reads nginx.conf through a file-level bind mount: test the new file in a fresh container as the Readme describes, then restart nginx.
  5. If the host has an account named admin with uid 1000, the next fix-rights.sh run changes the ownership of both checkouts and of private/environment as described above. If your deploy account has another name, set DEPLOY_USER in .env before that run, and make sure that account holds the backup credentials in its ~/.aws and, for Yandex Cloud DNS renewal, the yc CLI in its home.

Version 3.0.0

Choose a tag to compare

@paskal paskal released this 12 Sep 20:49
v3.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 DATABASE converts a database in place with one ALTER TABLE per table that needs it, generated from information_schema. Each character column outside both the selected collation and utf8mb4_bin is rewritten with its own type, nullability, default and comment; *_bin columns become utf8mb4_bin, all others utf8mb4_unicode_ci (or --collation utf8mb4_0900_ai_ci, the MySQL 8 default that fresh Bitrix installs use). A plain CONVERT TO CHARACTER SET would widen TEXT to MEDIUMTEXT and turn binary collations into the table collation, which breaks unique keys such as b_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, VARCHAR longer than 16383 characters, line breaks in a default, type or comment, backticks or control characters in table or column names, views, non-InnoDB tables, and NO_BACKSLASH_ESCAPES in the global sql_mode.
  • --dry-run writes 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.sh publishes 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 USER leaves 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 own sid in .settings_extra.php, otherwise this cleanup also hits production entries.

Upgrade notes

  1. config/mysql/my.cnf is a file-level bind mount, so the container keeps reading the old inode after the pull. docker restart --timeout 120 mysql makes 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.
  2. To convert an existing database: take a backup, stop the site and php-cron, run sudo ./scripts/convert-utf8mb4.sh --dry-run DATABASE and then without --dry-run, add the Bitrix settings from the Readme, reload php-fpm, start php-cron and reopen the site. The released script converted a 1.35 GB Bitrix database of 744 tables in about three minutes of ALTER; the window also holds the two fingerprint passes and the application checks, so rehearse on a restored copy to size it.
  3. Existing databases are not converted automatically. New databases created without an explicit charset or collation use the new server defaults; renew-dev.sh creates dev with the production database's charset and collation.

Version 2.2.0

Choose a tag to compare

@paskal paskal released this 12 Sep 10:34
v2.2.0
a837951

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-cron and mysql carry health checks in docker-compose.yml: nginx probes its stub_status vhost on port 8084, php opens a TCP connection to port 9000 (replacing the image's php --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/**.svg is served with Content-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 from upload/ needs to adjust the rule.
  • The scanner fast-404 list gains .old, .save and a trailing ~.

Upload deduplication

  • scripts/dedup-upload.sh replaces byte-identical files under web/prod/upload with hard links using hardlink from 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 full hardlink output with its exit status goes to logs/dedup-upload.log; --dry-run shows what a run would save. config/cron/host.cron runs 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

  1. Recreate nginx, php, php-cron and mysql after pulling so the health checks attach; the mysql recreate restarts the database.
  2. The deduplication needs hardlink from util-linux and flock on the host; the script exits with a message when either is missing.
  3. 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

Choose a tag to compare

@paskal paskal released this 11 Sep 22:42
v2.1.0
0d7eb15

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 two sonet_* 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 corresponding return 404 in bitrix.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.conf denies PHP to crawlers that ignore robots.txt and 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.sh builds the dev copies of dbconn.php and .settings.php from 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.sh runs under bash with pipefail and 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 under pipefail.
  • compare-backups.sh fails on a diff error instead of reporting a completed comparison, and restores into fresh directories.
  • config/zabbix/templates/backup-monitoring.yaml alerts on stale or empty backups once imported and linked to the host.

nginx

  • Bitrix composite snapshots are served directly by nginx, HEAD included. This is inert until composite is enabled in the Bitrix admin and snapshots exist; see "Composite site" in the README.

Operations

  • scripts/pull-public.sh is the production pull wrapper for this deployment: it handles the root-owned cron and logrotate directories, restarts php-cron only 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 on scripts/pull-public.sh covers 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-logs runs.
  • Bitrix cron stderr lands in cron_events.log.
  • Dev renewal runs private/scripts/renew-dev-post.sh from the overlay when present.
  • Optional Zabbix templates for the site automation live under config/zabbix/templates/, with their alert routing in config/zabbix/ALERT-ROUTING.md.

Upgrade notes

  1. Monitoring profile: private/environment/zabbix-mysql-session.conf is now a required bind for zabbix-agent, so the service cannot be recreated without it. Copy config/zabbix/mysql-session.conf.example there with your monitoring credentials, then in Zabbix set {$MYSQL.DSN} to LocalMysql and leave {$MYSQL.USER} and {$MYSQL.PASSWORD} empty (private/environment/zabbix.env.example).
  2. FTP moved to the ftp-manual profile with restart: no; start it on demand and do not add the profile to COMPOSE_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.
  3. 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".
  4. scripts/mysql-dump.sh is a bash script. Run it as an executable, as the shipped cron line does, not as sh scripts/mysql-dump.sh.
  5. binlog_expire_logs_seconds in config/mysql/my.cnf is 432000 (five days); it was 18000, five hours, too short to bridge two dumps. Apply it live with SET GLOBAL binlog_expire_logs_seconds=432000 or restart MySQL, and allow disk for five days of your binlog volume.

Version 2.0.0

Choose a tag to compare

@paskal paskal released this 11 Jun 12:29
465fb20

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

  1. Site vhosts, redirect maps, Metrika cookie pinning, CSP snippets, four Zabbix templates, PHPStan scopes, site cron jobs, the updater task list and bin/yandex-reviews left this repository. If you customised any of them, move your versions into your own overlay following the README pattern.
  2. container_name: is gone from compose: docker exec nginx ... becomes docker exec <project>-nginx-1 ..., or pin names in your own docker-compose.override.yml.
  3. Values now come from .env (see .env.example); check the defaults against your deployment: MYSQL_DATABASE defaults to bitrix (was favor_group_ru), DOCKER_MTU to 1500 (was 1450 — Yandex Cloud DDoS-protected IPs need 1450), HTTP_PORT/HTTPS_PORT to 80/443, TZ to Europe/Moscow (unchanged).
  4. nginx carries no TLS material: certificate paths moved out of the shared files — declare them per vhost in your overlay (ssl.conf include pattern). CSP includes are now globs that tolerate absence; hotlink protection, X-Frame-Options and admin CORS are driven by maps extendable via private.conf.d/*.map. FastCGI HTTP_HOST uses the port-preserving $bitrix_host map.
  5. Scripts now read site values from env files: backup scripts need private/environment/backup.env, seo-reindex.sh needs QUOTA_HOST in its env, urls.py needs SITE_URL and writes the redirect map to the overlay path.
  6. New: localhost demo vhost, AGENTS.md + llms.txt for coding agents, and a validate CI workflow guaranteeing a fresh clone always passes nginx -t and compose config.

Version 1.0.0

Choose a tag to compare

@paskal paskal released this 10 Jun 23:18

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.