А что если… нужен старый софт, но при этом хост Linux свежий?
Как вариант, можно взять, да хоть на примере питона или php, нужный официальный образ в докере. Да и пользоваться.
Казалось бы, проблема решена. Но есть нюанс ©. Добро пожаловать под кат.
Зачем это всё
Однажды мне понадобился Python 3.7. Потом 2.7. Потом — внезапно — 3.4 и даже 2.6 под легаси, которое никто не собирается переписывать. И каждый раз вставали одни и те же вопросы:
Что делать, если не устраивают готовые образы старых версий?
Что, если старую версию надо запустить на хосте без контейнеризации?
Что, если бинарей нет, или они ссылаются на старую glibc, и собирать придётся из исходников?
Чем мне не нравятся готовые образы со старыми версиями:
раскатаны они, как правило, на старых Debian/Ubuntu, которые давно сами в EOL;
старый дистрибутив — это старые библиотеки, старые зависимости и старые уязвимости;
я уже говорил про glibc?
а совсем древние дистрибутивы ещё и на свежем ядре заводятся не всегда (vsyscall, загрузчик и прочие радости).
Объективно, тут надо выбрать компромисс:
Либо идём на сделку с дьяволом: изолируем рантайм, экономим время и пользуемся готовыми образами со старыми дистрами.
Либо идём на авантюру и собираем старый софт на современном Linux.
Я выбрал второй вариант. Дух авантюризма во мне пока не пропал.
Всё, о чём пойдёт речь, живёт в открытом репозитории моего pet‑проекта: там сейчас GCC с 4-й по 16-ю версию и CPython от 2.6 до 3.15 и многое другое, и почти все собраны поверх Debian 13 trixie. Всё это я собирал сам, вечерами. Ниже — выжимка с граблями, на которые я наступил по дороге.
Часть первая. Компилятор
Скорее всего, вы скажете: есть же Makefile и INSTALL — вперёд и с песней!
Добро пожаловать в dependency hell, отвечу я вам.
Начнём с компилятора. В Debian 13 из коробки идёт GCC 14. Как и сами языки C/C++, компиляторы со временем становятся строже: то, что было warning‑ом, становится error‑ом, меняются стандарты по умолчанию. Возьмите Python 3.4 и соберите его GCC 14 — словите пачку ошибок.
«Но можно же написать лапшу из флагов!» — скажете вы. Можно, но работать это будет частично или номинально.
Поэтому я собрал старый GCC.
Лесенка версий
Новый GCC обычно без проблем собирает предыдущий мажор, а тот — свой предыдущий. Так что первоначальный цикл у меня был такой:
Беру системный GCC 14 в Debian 13 и собираю им GCC 13. Публикую образ
gcc:13.На всякий случай пересобираю GCC 13 уже им самим и публикую в тот же тег. Теперь в образе компилятор, который точно умеет собрать сам себя.
Повторяю вниз до GCC 4. Глубже копать не стал.
Это разовая история. Когда лесенка пройдена, каждый образ gcc:N пересобирается из самого себя: FROM gcc:10 AS builder собирает очередной минорный релиз 10.x. Поэтому в Dockerfile ниже стоит --disable-bootstrap: трёхстадийная самопроверка GCC уже не нужна, её роль сыграла лесенка.
Сокращённый Dockerfile для GCC 10 (полный — apps/gcc/10):
FROM ghcr.io/epicmorg/gcc:10 AS builder ARG GCC_MAJOR_VERSION=10 ARG GCC_MINOR_VERSION=5.0 ENV GCC_VERSION=${GCC_MAJOR_VERSION}.${GCC_MINOR_VERSION} ENV GCC_INSTALL_DIR=${EMG_LOCAL_BASE_DIR}/gcc/${GCC_MAJOR_VERSION} RUN set -eux; \ aria2c -d /tmp "https://gcc.gnu.org/pub/gcc/releases/gcc-${GCC_VERSION}/gcc-${GCC\_VERSION}.tar.xz" && \ tar -xf /tmp/gcc-${GCC_VERSION}.tar.xz -C /tmp && \ cd /tmp/gcc-${GCC_VERSION} && ./contrib/download_prerequisites && \ mkdir -p /tmp/gcc-build && cd /tmp/gcc-build && \ /tmp/gcc-${GCC_VERSION}/configure \ --prefix=${GCC_INSTALL_DIR} \ --enable-languages=c,c++ \ --disable-multilib \ --disable-bootstrap \ --enable-checking=release \ --enable-multiarch \ --with-multiarch-defaults=x86_64-linux-gnu \ --enable-default-pie \ --enable-shared \ --enable-threads=posix \ --with-system-zlib && \ make -j$(nproc) && make install ENV PATH="${GCC_INSTALL_DIR}/bin:${PATH}" # Проверка: компилятор видит multiarch-заголовки Debian RUN echo '#include <bits/wordsize.h>' | gcc -E - >/dev/null FROM ghcr.io/epicmorg/debian:trixie-develop COPY --from=builder ${GCC_INSTALL_DIR} ${GCC_INSTALL_DIR} ENV PATH="${GCC_INSTALL_DIR}/bin:${PATH}"
Обратите внимание: у меня компилятор живёт в своём префиксе /usr/local/share/epicmorg/gcc/10, а не в /usr. Системный GCC 14 никуда не делся.
Грабли компилятора
Multiarch. Debian кладёт архитектурно‑зависимые заголовки в
/usr/include/x86_64-linux-gnu. GCC, собранный без--enable-multiarch --with-multiarch-defaults=x86_64-linux-gnu, туда не заглядывает, и дальше вы получаетеgmp.h: No such file or directoryи прочие чудеса.Триплеты. GCC 6 и старше ломаются от явных
--build/--host/--target(This configuration is not supported…). Для них я триплеты не передаю, пусть configure определит сам.gcc≠cc. В образеgcc— это мой компилятор, аcc— системный Debian. Autoconf разных проектов ищет их в разном порядке: Python первым находитgcc, PHP —cc. Это работает, но не стоит гадать, кто реально собирал — проверяйтеsys.version. И не надо насильно ставитьCC=gcc11«для единообразия»: старый компилятор не умеет линковаться с системными библиотеками trixie, и у PHP 7.3 после этого падает вообще каждый тест configure.libstdc++ разных версий. Если библиотеку собрал GCC 14, а линкуете вы её GCC 11 — получите
undefined reference to …@GLIBCXX_3.4.30. Правило простое: кто собирал зависимость, тот собирает и всё, что с ней линкуется.
С компилятором разобрались. Переходим к практике.
Часть вторая. Старый софт и ловушка зависимостей
Итак, у меня есть GCC 10, который переварит исходники Python 3.7. Казалось бы, делай ./configure && make и открывай шампанское.
Но тут меня ждал финальный босс — системные библиотеки. А самый коварный из них — OpenSSL.
В Debian 13 идёт OpenSSL 3.x. API/ABI у него обновились, и Python 3.7 с ним не дружит. Если просто запустить сборку, питон радостно скомпилируется, а в конце скажет, что модуль ssl не собран. Питон без SSL в современных реалиях бесполезен: ни pip install, ни HTTPS‑запроса.
Значит, OpenSSL тоже собираю сам. Под каждую версию Python своя связка — вот матрица из репозитория:
Python |
OpenSSL |
GCC |
pip / setuptools |
Заметки |
|---|---|---|---|---|
2.6 |
1.0.2 u |
4.9 |
9.0.3 / 36.8.0 |
нет SNI |
2.7 |
1.1.1 w |
11 |
20.3.4 / 44.1.1 |
|
3.0, 3.1 |
1.0.2 u |
4.9 |
— |
нет pip, нет SNI |
3.2 |
1.0.2 u |
4.9 |
7.1.2 / 29.0.1 |
|
3.3 |
1.0.2 u |
7 |
10.0.1 / 39.2.0 |
|
3.4 |
1.0.2 u |
7 |
19.1.1 / 43.0.0 |
|
3.5 |
1.1.1 w |
7 |
20.3.4 / 50.3.2 |
|
3.6 |
1.1.1 w |
9 |
21.3.1 / 59.6.0 |
|
3.7 |
1.1.1 w |
10 |
24.0 / 68.0.0 |
|
3.8 |
1.1.1 w |
10 |
25.0.1 / 75.3.2 |
|
3.9 |
1.1.1 w |
10 |
26.0.1 / 80.9.0 |
|
3.10 — 3.12 |
3.5.x (LTS) |
13 |
актуальные |
|
3.13 — 3.14 |
3.5.x (LTS) |
14 |
актуальные |
|
3.15 |
3.5.x (LTS) |
15 |
актуальные |
pre‑release |
OpenSSL 1.0.2 в EOL с конца 2019 года, 1.1.1 — с сентября 2023-го. Для новых питонов я беру 3.5 именно потому, что это LTS (до 2030 года). 3.6 и 4.0 сознательно пропускаю.
Куда класть старые библиотеки?
Вот тут главная архитектурная развилка.
Вариант 1: /usr/lib, /usr/local/lib или /etc/ld.so.conf.d. Идеальный способ выстрелить себе в ногу. Рано или поздно системная утилита подтянет вашу libssl вместо своей — и привет.
Вариант 2: LD_LIBRARY_PATH. Костыль, который мы все так любим. Он действует на процесс и на всех его потомков: питон вызвал curl или git — и они тоже пошли за вашим старым OpenSSL.
Вариант 3: статическая линковка. Обновить одну зависимость после очередной CVE — значит пересобрать всё, что с ней слинковано. Да и не всякая библиотека на это соглашается.
Я пошёл четвёртым путём. Каждая зависимость ставится в свой префикс /usr/local/share/epicmorg/<имя>/<версия>, а пути к ним вшиваются прямо в ELF через RPATH:
-Wl,-rpath,/usr/local/share/epicmorg/openssl/1.1.1/lib
Загрузчик (ld.so) прочитает путь из заголовка бинарника и найдёт библиотеку без всяких переменных окружения. Система остаётся чистой, а мой старый софт ходит со своими костылями.
Часть третья. RPATH, который оказался RUNPATH
Вот тут было больно, поэтому подробно.
В секции .dynamic ELF‑файла путь поиска может лежать в одном из двух тегов:
|
|
|
|---|---|---|
Приоритет относительно |
выше |
ниже |
Действует на зависимости зависимостей |
да |
нет |
-Wl,-rpath — это просто «записать путь». А в какой тег его запишет линкер, определяет флаг --enable-new-dtags / --disable-new-dtags. И линкер в Debian по умолчанию пишет RUNPATH, хотя вы ничего такого не просили.
Чем это кончается? Классика: libicuuc нашла себя по RUNPATH, а libicudata, которую загружает уже сама libicuuc, — нет. RUNPATH не распространяется на транзитивные зависимости, и на этапе make вы получаете
libicudata.so.73: cannot open shared object file: No such file or directory
хотя -rpath указывает куда надо.
Лечение — явно рядом с каждым -rpath добавлять -Wl,--disable-new-dtags и проверять результат:
readelf -d python3.7 | grep -E 'RPATH|RUNPATH|NEEDED'
Отдельный сюрприз от Python. В distutils (а через него собираются модули вроде sqlite3) функция runtimelibrary_dir_option() сама подставляет -Wl,--enable-new-dtags,-R<dir>. То есть вы всё сделали правильно, а часть модулей всё равно получила RUNPATH. Приходится патчить:
RUN f=Lib/distutils/unixccompiler.py; \ if grep -q -- '--enable-new-dtags' "$f"; then \ sed -i 's/--enable-new-dtags/--disable-new-dtags/g' "$f"; \ fi
Часть четвёртая. Сборка Python 3.7
Пайплайн — multi‑stage Dockerfile. Zlib и OpenSSL 1.1.1 w уже лежат в базовом build‑образе (debian:trixie-develop, от которого наследуется gcc:10), каждый в своём префиксе и с собственным RPATH. Сам OpenSSL собирается вот так:
CFLAGS="-Wno-error -fPIC" ./config \ zlib --with-zlib-include=${ZLIB_DIR}/include --with-zlib-lib=${ZLIB_DIR}/lib \ enable-ec_nistp_64_gcc_128 enable-ssl-trace threads shared \ -Wl,-rpath,${OPENSSL_111_LIB_DIR} \ -Wl,-rpath,${ZLIB_LIB_DIR} \ -Wl,--disable-new-dtags \ --release --prefix=${OPENSSL_111_DIR}
Обратите внимание: RPATH на zlib прописан и в самом libssl. Иначе «мой» OpenSSL внутри питона загрузит системный libz.
Сокращённый builder‑слой Python (полный — apps/python/3.7):
FROM ghcr.io/epicmorg/gcc:10 AS builder ENV PYTHON_VERSION=3.7 ENV PYTHON_FULL_VERSION=${PYTHON_VERSION}.17 ENV PYTHON_DIR=${EMG_LOCAL_BASE_DIR}/python/${PYTHON_VERSION} # Проверяем, что базовый образ действительно отдал нам OpenSSL и zlib RUN : "${OPENSSL_111_DIR:?empty}" "${OPENSSL_111_LIB_DIR:?empty}" "${ZLIB_LIB_DIR:?empty}" # Запрещаем apt тянуть дистрибутивный python COPY etc/apt/preferences.d /etc/apt/preferences.d RUN apt-get update && apt-get install -y \ build-essential pkg-config libffi-dev libncurses-dev libreadline-dev \ libsqlite3-dev libgdbm-dev libbz2-dev libexpat1-dev liblzma-dev uuid-dev tk-dev # Убираем с дороги системные заголовки OpenSSL 3.x RUN mv /usr/include/openssl /usr/include/openssl.disabled # ... скачивание исходников и патч distutils из предыдущей части ... RUN set -eux; \ export LDFLAGS="-L${OPENSSL_111_LIB_DIR} -L${ZLIB_LIB_DIR} \ -Wl,-rpath,${OPENSSL_111_LIB_DIR} \ -Wl,-rpath,${PYTHON_DIR}/lib \ -Wl,-rpath,${ZLIB_LIB_DIR} \ -Wl,--disable-new-dtags" && \ export CPPFLAGS="-I${OPENSSL_111_INC_DIR} -I${ZLIB_INC_DIR}" && \ ./configure \ --prefix=${PYTHON_DIR} \ --enable-shared \ --enable-optimizations \ --with-lto \ --enable-option-checking=fatal \ --enable-loadable-sqlite-extensions \ --with-ensurepip=install \ --with-computed-gotos \ --with-openssl=${OPENSSL_111_DIR} && \ make -j$(nproc) && \ make altinstall && \ ${PYTHON_DIR}/bin/python${PYTHON_VERSION} -m pip install --upgrade pip setuptools
Пара моментов:
mv /usr/include/openssl …— страховка от самой подлой ошибки: собрались с заголовками системного OpenSSL 3.x, а слинковались с моим 1.1.1. Такое собирается, запускается и падает в проде.--enable-option-checking=fatal— если вы выдумали флаг configure (или он исчез в этой версии), сборка упадёт, а не проигнорирует его молча. Флаги вообще стоит сверять с./configure --helpконкретной версии: они появляются и исчезают в обе стороны.
Финальный слой — чистый debian:trixie, в который копируются префиксы Python, OpenSSL и zlib:
FROM ghcr.io/epicmorg/debian:trixie COPY --from=builder ${PYTHON_DIR} ${PYTHON_DIR} COPY --from=builder ${OPENSSL_111_DIR} ${OPENSSL_111_DIR} COPY --from=builder ${ZLIB_DIR} ${ZLIB_DIR} # Для тех, кто будет собирать C-расширения против нашего OpenSSL ENV CPPFLAGS="-I${OPENSSL_111_DIR}/include" ENV LDFLAGS="-L${OPENSSL_111_LIB_DIR} -Wl,-rpath,${OPENSSL_111_LIB_DIR} -Wl,--disable-new-dtags" RUN update-alternatives --install /usr/bin/python3 python3 ${PYTHON_DIR}/bin/python${PYTHON_VERSION} 1 && \ update-alternatives --install /usr/bin/pip3 pip3 ${PYTHON_DIR}/bin/pip${PYTHON_VERSION} 1
«А почему в финальном образе есть build-essential и -dev пакеты?» — спросит внимательный читатель. Потому что у части пакетов (psycopg2, pycurl, lxml, python‑ldap…) под старые питоны нет колёс, и pip install будет компилировать их на месте. Сам Python этим компилятором не собирается — это делает builder.
Часть пятая. Доверяй, но проверяй
Представьте, ABI‑дрейф не шумит. ваш образ, слинкованный не с той библиотекой, отлично собирается, проходит smoke‑тест и падает уже на проде. Неприятно. Поэтому у меня сборку валит каждая из этих проверок — прямо в Dockerfile, и в builder‑слое, и в финальном:
py=${PYTHON_DIR}/bin/python3.7 # 1. С какими заголовками компилировали (0x1010… = OpenSSL 1.1.x) $py -c 'import _ssl; print(hex(_ssl.OPENSSL_VERSION_NUMBER))' # 2. Какая библиотека реально загрузилась $py -c 'import ssl; print(ssl.OPENSSL_VERSION)' # Если 1 и 2 расходятся — собрали с чужими заголовками # 3. _ssl и _hashlib линкуются из нашего префикса и без "not found" for m in _ssl _hashlib; do so=$($py -c "import $m; print($m.__file__)") ldd "$so" | grep -q 'not found' && exit 1 ldd "$so" | grep -q "${OPENSSL_111_LIB_DIR}/lib" || exit 1 done # 4. Везде RPATH и нигде RUNPATH for f in $py ${PYTHON_DIR}/lib/libpython*.so.* ${PYTHON_DIR}/lib/python3.7/lib-dynload/*.so; do readelf -d "$f" | grep -q '(RUNPATH)' && exit 1 readelf -d "$f" | grep -q '(RPATH)' || exit 1 done # 5. В процессе ровно один libz, и он наш $py -c 'import ssl, zlib; print(open("/proc/self/maps").read())' | awk '/libz\.so/ {print $NF}' | sort -u
Обратите внимание: смотреть надо ldd модуля _ssl, а не самого python3. Бинарник интерпретатора с libssl не линкуется вообще, SSL приезжает с подгружаемым модулем.
И главное правило: две версии одной библиотеки в одном процессе — это всегда баг. Если ldd показывает и libssl.so.1.1, и libssl.so.3, ищите, кто слинковался не с тем префиксом.
Результат — docker run --rm -it ghcr.io/epicmorg/python:3.7:
$ head -1 /etc/os-release PRETTY_NAME="Debian GNU/Linux 13 (trixie)" $ python3 -V Python 3.7.17 $ python3 -c 'import sys; print(sys.version)' 3.7.17 (default, Oct 25 2026, 09:09:48) [GCC 10.5.0] $ pip --version pip 24.0 from /usr/local/share/epicmorg/python/3.7/lib/python3.7/site-packages/pip (python 3.7) $ python3 -c 'import ssl; print(ssl.OPENSSL_VERSION)' OpenSSL 1.1.1w 11 Sep 2023 $ python3 -c 'import _ssl; print(hex(_ssl.OPENSSL_VERSION_NUMBER))' 0x1010117f
Заголовки 1.1.1 (0x1010117f), библиотека 1.1.1 w, собрано GCC 10.5. Теперь линковка модуля _ssl:
$ ldd $(python3 -c 'import _ssl; print(_ssl.__file__)') linux-vdso.so.1 (0x00007fce02884000) libssl.so.1.1 => /usr/local/share/epicmorg/openssl/1.1.1/lib/libssl.so.1.1 (0x00007fce0279e000) libcrypto.so.1.1 => /usr/local/share/epicmorg/openssl/1.1.1/lib/libcrypto.so.1.1 (0x00007fce02444000) libpython3.7m.so.1.0 => /usr/local/share/epicmorg/python/3.7/lib/libpython3.7m.so.1.0 (0x00007fce02114000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fce01f14000) libz.so.1 => /usr/local/share/epicmorg/zlib/1.3.1/lib/libz.so.1 (0x00007fce01ef5000) libcrypt.so.1 => /lib/x86_64-linux-gnu/libcrypt.so.1 (0x00007fce01eb9000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fce01dc9000) /lib64/ld-linux-x86-64.so.2 (0x00007fce02886000) $ readelf -d $(python3 -c 'import _ssl; print(_ssl.__file__)') | grep -E 'RPATH|RUNPATH' 0x000000000000000f (RPATH) Library rpath: [/usr/local/share/epicmorg/openssl/1.1.1/lib:/usr/local/share/epicmorg/python/3.7/lib:/usr/local/share/epicmorg/zlib/1.3.1/lib]
libssl, libcrypto и даже libz — из моих префиксов, системные только libc, libm и libcrypt. В заголовке — именно RPATH, не RUNPATH. А вот сам интерпретатор, как и обещал, с OpenSSL не линкуется вовсе:
$ ldd $(command -v python3.7) linux-vdso.so.1 (0x00007f270d4c8000) libpython3.7m.so.1.0 => /usr/local/share/epicmorg/python/3.7/lib/libpython3.7m.so.1.0 (0x00007f270d18d000) libcrypt.so.1 => /lib/x86_64-linux-gnu/libcrypt.so.1 (0x00007f270d147000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f270d057000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f270ce63000) /lib64/ld-linux-x86-64.so.2 (0x00007f270d4ca000)
И вишенка — два OpenSSL в одном контейнере, которые друг другу не мешают:
$ /usr/bin/openssl version OpenSSL 3.5.7 9 Jun 2026 (Library: OpenSSL 3.5.7 9 Jun 2026) $ openssl version OpenSSL 1.1.1w 11 Sep 2023
Системный openssl и всё, что ставится через apt, работают с 3.5 из Debian. Мой питон — с 1.1.1 w.
Бонус: музей (Python 2.6, 3.0–3.6)
Совсем старые версии на glibc 2.41 и ncurses 6 требуют точечных правок. Только на этапе сборки, рантайм не трогаю:
glibc 2.41 объявляет C23-функцию
sinpi(), а вModules/mathmodule.cесть своя staticsinpi. Ошибкаstatic declaration of 'sinpi' follows non-static declaration, модульmathмолча не собирается, а за ним ломаютсяrandom,tempfileиensurepip.
Лечится переименованием, как сделали в upstream начиная с 3.7:sh sed -i 's/\bsinpi\b/m_sinpi/g' Modules/mathmodule.cncurses 6 делает
WINDOWнепрозрачным (NCURSES_OPAQUE=1), а старыйpy_curses.hлезет вwin->_flags. Бэкпортировал защиту из 3.6.Нет SNI в
sslу 2.6, 3.0 и 3.1. На PyPI по HTTPS без SNI не попасть, так чтоpip installиз интернета там не работает — только локальные wheel/sdist. У 3.0 и 3.1 pip вообще нет.Кое‑что так и не собирается:
nis(в glibc больше нетrpc/rpc.h),dbmна 3.1–3.6,tkinterна 2.6/3.0,lzmaдо 3.3.
Не только Python: PHP и патч, которого не было
Всё описанное выше работает не только для питона. На том же конвейере я собираю PHP с 5.3 по 8.5. Базовый образ тот же epicmorg/gcc, и та же лесенка OpenSSL:
PHP |
OpenSSL |
Почему |
|---|---|---|
5.3–5.6 |
1.0.2 u |
поддержка OpenSSL 1.1 появилась только в PHP 7.1 |
7.0–8.0 |
1.1.1 w |
OpenSSL 3.x надёжно работает только с 8.1 |
8.1–8.5 |
3.5.x |
LTS‑ветка OpenSSL, поддержка до 2030 года |
Те же приёмы: прячу системные заголовки OpenSSL от ./configure, собираю свои ICU, libcurl и libpq, если системные тянут OpenSSL 3, и прописываю RPATH. Подробности в README образа PHP.
А теперь история, ради которой всё это стоило затевать.
В октябре 2026 года вышли сразу две уязвимости в ядре PHP: CVE-2026-109618 и CVE-2026-109619, в народе PHP2Shell. Мир, мягко говоря, напрягся. 21 октября PHP выпустил исправленные 8.2–8.5. С EOL‑ветками сложнее. Новых релизов 7.4, 8.0 и 8.1 не будет, их поддержка закончилась. Но разработчики PHP выложили для них минимальный фикс «Avoid state corruption via bailout» в отдельные ветки php‑src. Тарбола не было, только коммит.
Дальше всё зависит от того, откуда у вас PHP.
Если от вендора, вы ждёте. На Debian и Ubuntu де‑факто стандарт для PHP — репозиторий Ondřej Surý (sury.org). С июля 2024 года его автор геоблокирует Россию на packages.sury.org (issue #2155), поэтому из РФ патчи приходилось ждать вдвойне. А для 7.4–8.1 этот фикс в репозитории так и не появился. Кто сидит на этих версиях, тот остался с дырой.
Если сборка своя, для поддерживаемых веток достаточно поднять номер версии и контрольную сумму:
ENV PHP_FULL_VERSION=${PHP_VERSION}.27 # Upstream sha256 for php-8.4.27.tar.xz - bump together with PHP_FULL_VERSION. ARG PHP_SHA256=dcf93923b0e38e0d5252b25b796520b5a47761a08b3488b6d0b5d1115764d3f0
Для EOL‑веток я кладу коммит из php‑src рядом с Dockerfile и накатываю его на исходники последнего релиза. Любой конфликт патча валит сборку:
# PHP_PATCHES: applied in name order, any reject is fatal. COPY tmp/patches/ /tmp/php-patches/ RUN set -eux; \ for p in /tmp/php-patches/*.patch; do \ patch -d ${PHP_SRC_DIR} -p1 --forward --no-backup-if-mismatch < "$p"; \ done; \ if find ${PHP_SRC_DIR} -name "*.rej" | grep -q .; then echo "FATAL: patch rejects" >&2; exit 1; fi; \ rm -rf /tmp/php-patches
Теги остаются прежними (7.4.33 остается7.4.33), а что именно наложено, видно в переменной окружения и в метке образа:
ENV PHP_PATCHES="CVE-2026-109618 CVE-2026-109619" LABEL org.epicmorg.php.patches="CVE-2026-109618 CVE-2026-109619"
Весь фикс — один коммит в тот же день, когда вышло объявление PHP: семь веток, от 7.4 до 8.5. Дальше CI пересобрал образы, а на проде оставалось только перекатить пару‑тройку контейнеров. Никакого ожидания вендора.
Для сравнения — что внутри официальных образов тех же версий на Docker Hub (замер на 30.10.2026):
Образ |
Последняя сборка |
База |
OpenSSL |
|---|---|---|---|
|
15.11.2022 |
Debian 11.5 |
1.1.1 n (март 2022) |
|
22.07.2023 |
Debian 12.1 |
3.0.9 (май 2023) |
|
25.10.2026 |
Debian 13 |
1.1.1 w + патч PHP2Shell |
Официальные образы EOL‑версий просто заморожены в день окончания поддержки: вместе с базой, системными пакетами и всеми уязвимостями, найденными с тех пор.
Проверка тезиса: переезд на Astra Linux
Через всю статью проходит одна мысль: если рантайм сам носит свой компилятор, OpenSSL и прочие зависимости, ему почти всё равно, на каком дистрибутиве жить. Лишь бы glibc была не старее нужной. Звучит красиво, поэтому я решил проверить это на практике.
У меня есть собственные неофициальные Docker‑образы Astra Linux 1.7 и 1.8. Я взял develop‑образ Astra 1.8.6 (glibc 2.36, база Debian 12) и перенёс на него те же цепочки, что описаны выше: GCC 10 и 14, Python 3.7 и 3.14, PHP 7.4 (с патчем PHP2Shell) и PHP 8.5. Пушить ничего не стал, всё собирал локально.
Результат:
Цепочка |
Итог |
Что пришлось поменять, кроме |
|---|---|---|
GCC 10.5 и 14.4 |
✅ |
ничего: собрались прямо системным GCC 12 из Astra, лесенка не понадобилась |
Python 3.7 и 3.14 |
✅ |
ничего |
PHP 7.4 |
✅ |
системная libxml2 в Astra собрана с ICU 72, поэтому intl тоже пришлось перевести на ICU 72 |
PHP 8.5 |
✅ |
явно добавил |
Все проверки из пятой части прошли без поблажек: каждая библиотека грузится из своего префикса, в процессе ровно один OpenSSL, HTTPS работает.
Самое ценное вылезло с PHP 8.5. Системная libhashkit (её тянет расширение memcached) зависит от libcrypto и при этом несёт RUNPATH — помните третью часть? RPATH нашего модуля на неё не действует, и она подтягивала системный OpenSSL. На Debian это маскировалось случайностью: libsasl2 успевала загрузить наш libcrypto раньше. В Astra libsasl2 собрана без libcrypto, и проблема проявилась сразу. Исправление — одна строка, и оно нужно в том числе для Debian. Так что Astra заодно нашла мне баг, который на «родной» базе просто ещё не выстрелил.
Гипотеза подтвердилась: четыре рантайма переехали на другой дистрибутив с двумя точечными правками, и обе касались системных библиотек, а не самого подхода.
Честные оговорки:
Это develop‑образ в Docker, а не настоящий сертифицированный хост. Как поведёт себя замкнутая программная среда (проверка цифровых подписей бинарников), я не проверял. А мои бинарники ничем не подписаны.
Свой OpenSSL рядом с системным не входит в сертифицированную конфигурацию Astra. Если вам важен именно сертификат, этот подход вам вряд ли подойдёт.
Astra 1.7 (glibc 2.28) я пока не трогал. Там, скорее всего, понадобится та самая лесенка компиляторов.
Выводы
Стоит ли оно того? Если нужно быстро поднять проект «на отвали» — берите готовый образ и не мучайтесь.
Но если вы строите инфраструктуру, которая должна жить долго и быть предсказуемой (насколько это возможно со старым софтом), изоляция зависимостей и сборка из исходников — ваш путь.
Что я получил:
Современная база. Свежее ядро, Debian 13, glibc 2.41. Никакой эмуляции старых syscall‑ов.
Чистая система. В
/usr/libиld.so.confя ничего не подкладываю. Системный софт работает с системным OpenSSL 3.x, мой Python 3.7 — со своим 1.1.1 w.Контроль. Нашёл уязвимость — патчу исходники и пересобираю в своём пайплайне, не дожидаясь, пока кто‑то обновит образ на Docker Hub (спойлер: для EOL‑софта никто не обновит). История с PHP2Shell это наглядно показала: свой конвейер закрыл дыру за вечер, а пользователи EOL‑веток из внешних репозиториев остались без патча.
Независимость. Внешний репозиторий может отстать, забросить старую ветку или просто закрыть доступ для вашей страны. Исходники и контрольные суммы, которые лежат у вас, никуда не денутся.
Один парк вместо зоопарка. Python с Docker Hub, PHP из sury, Node от ещё одного вендора — это разные базовые образы (Alpine, Bullseye, Bookworm, Ubuntu), разные libc и разные версии OpenSSL. Каждый тащит свои слои и свой набор уязвимостей. У меня одна база,
epicmorg/debian:trixie, под всеми рантаймами. Обновил базу — обновились все.Переносимость. Тот же рецепт почти без правок переехал на Astra Linux. Рантайм зависит от моего конвейера, а не от конкретного дистрибутива.
Проверяемость. Если линковка поехала, образ просто не соберётся.
Сразу оговорюсь: я не продаю свои образы как решение, а использую как наглядный пример. Они у меня пока не такие лёгкие, как вендорские: в них стоит чуть больше софта и зависимостей для более широкого круга дочерних образов. Но это частности. В статье я рассказываю способ. Подойдут мои образы — берите. Не подойдут — соберите у себя по тем же рецептам, все Dockerfile открыты.
Да, на старте придётся потратить время на матрицу совместимости и на грабли. Это пара вечеров на каждый рантайм. Зато потом каждое обновление и каждый срочный патч — это поменять номер версии и дождаться CI. Один раз выстроив этот конвейер, вы получаете железобетонный и переносимый рантайм и экономите себе на перспективу необъятное количество времени.
Не бойтесь заглядывать под капот линкера. Иногда старый добрый RPATH (именно RPATH, а не RUNPATH!) спасает нервы лучше любых контейнеров.
Все Dockerfile из статьи — в EpicMorg/docker, образы на Docker Hub, quay.io и ghcr.io.
Комментарии (12)

MrBotikkk
30.10.2026 14:19У меня есть собственные неофициальные Docker-образы Astra Linux 1.7 и 1.8.
Есть официальные в открытом доступе. Просто никто не читает документы. "Образы доступны для скачивания по ссылке в разделе astra."
Есть специальный проект https://hub.docker.com/r/debian/eol
debian/eol:bullseye debian/eol:buster debian/eol:stretch и возможно debian/eol:jessieбудут запускаться на современных ядрах. Python 3.7 и ниже не проблема.
Stam_emg Автор
30.10.2026 14:19Спасибо за ссылки , учту.
Про Astra.
Свои образы я делал давно, когда официальных в открытом доступе ещё не было, во всяком случае для меня. С тех пор просто поддерживаю их в том же виде, что и остальные свои образы. Сам я не пользователь Astra, и кому нужна именно официальная, тот, конечно, возьмёт официальную. Этой мысли посвящен абзац.
Про debian/eol.
Я им пользовался в своих образах, пока на нём не начали копиться CVE. Статья как раз о том, почему я от этого ушёл: debian/eol — это тот же старый зоопарк, только аккуратно упакованный. Старый OpenSSL, старая glibc, обновлений безопасности больше нет. А совсем старые релизы вроде squeeze и wheezy (6 и 7) на свежих ядрах без эмуляции vsyscall не заводятся, о чём я тоже упоминал в начале статьи.

MrBotikkk
30.10.2026 14:19Запускал на Debian 13
docker.io/debian/eol:lennyecho 'GRUB_CMDLINE_LINUX_DEFAULT="${GRUB_CMDLINE_LINUX_DEFAULT} vsyscall=emulate"' | sudo tee /etc/default/grub.d/99-vsyscall.cfg sudo update-grub ----- sudo rm /etc/default/grub.d/99-vsyscall.cfg sudo update-grub
Stam_emg Автор
30.10.2026 14:19это верно. запустить можно. и именно то что вы делаете, написано в моем комментарии выше. эмуляция сисколов и изменение параметров загрузчика.
в целом, данная ветка немного не про тематику статьи. складывается впечатление.
доводы которые вы приводите изложены еще в первых абзацах. и достаточно подробно аргументированны. и почему от этого отказался.

MrBotikkk
30.10.2026 14:19Не сочтите за “Грубое общение”, но конкретных решений в статье по vsyscall=emulate нет. Мной такой подход тестировался 3-4 г. назад
Dockerfile eol:lenny etch
FROM docker.io/debian/eol:lenny ENV DEBIAN_FRONTEND=noninteractive ENV TERM=xterm RUN mkdir -p /etc/apt/sources.list.d \ && printf '%s\n' \ 'deb [trusted=yes] http://archive.debian.org/debian etch main' \ 'deb [trusted=yes] http://archive.debian.org/debian-security etch/updates main' \ > /etc/apt/sources.list.d/etch.list RUN mkdir -p /etc/apt/preferences.d \ && printf '%s\n' \ 'Package: *' \ 'Pin: release n=etch' \ 'Pin-Priority: 100' \ > /etc/apt/preferences.d/etch RUN mkdir -p /var/lib/apt/lists/partial \ && apt-get update \ && apt-get install -y --no-install-recommends nano file \ && rm -rf /var/lib/apt/lists \ && mkdir -p /var/lib/apt/lists/partial CMD ["/bin/bash", "--noprofile", "--norc", "-i"]Все эти версии Python уже не поддерживаются:
Python 3.9 https://peps.python.org/pep-0596/ Python 3.8 https://peps.python.org/pep-0569/ Python 3.7 https://peps.python.org/pep-0537/ Python 3.6 https://peps.python.org/pep-0494/ Python 2.7 https://www.python.org/doc/sunset-python-2/Отлично собираются в podman/docker Arch Linux image c последними доступными патчами

Stam_emg Автор
30.10.2026 14:19все подробно описано в первом абзаце.
то что вы делаете - рабочее. и никто не спорит. но в статье от этого пути был осознанный отказ. и статья не про это.
если честно, мне нечего больше ответить, тк я не понимаю что вы пытаетесь доказать :) точнее, зачем.

MrBotikkk
30.10.2026 14:19Кто обнуляет карму? сегодня в 17:52 -1 Грубое общение. Где проявилось мое грубое общение?

SilverTrouse
30.10.2026 14:19У меня есть немного обратная проблема. Astra 1.7 к нему хочу собрать допустим современный vscodium - в итоге собираем по цепочке binutils ( так-как родной ld чуть багованный и не может собрать gcc 14-15-16). Собираем gcc собираем python собираем nodejs собираем llvm собираем rust. Только уже после билдим vscodium
Пребинари не подходят так как libc нужен более современный (1.7 - debian 10)

MrBotikkk
30.10.2026 14:19mkdir -p ~/.local/bin cd ~/.local/bin wget https://github.com/VSCodium/vscodium/releases/download/1.143.17232/VSCodium-1.143.17232-anylinux-x86_64.AppImage chmod +x VSCodium-*.AppImage ln -sf ~/.local/bin/VSCodium-1.143.17232-anylinux-x86_64.AppImage ~/.local/bin/codium echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrcили
cd /tmp wget https://github.com/VSCodium/vscodium/releases/download/1.143.17232/VSCodium-1.143.17232-anylinux-x86_64.AppImage chmod +x VSCodium-*.AppImage sudo mv VSCodium-*.AppImage /usr/local/bin/codium

MrBotikkk
30.10.2026 14:19https://wiki.astralinux.ru/kb/sravnenie-sistem-upravleniya-paketami-snap-i-flatpak-238748637.html
flatpak install flathub com.vscodium.codium
jorlyf
Вы пробовали использовать Nix для получения нужной версии GCC/python? По идее, можно просто зафиксировать коммит репозитория пакетов на то время, когда нужная версия была актуальной, и Nix сам скачает или соберет готовую среду.
И перенос с Debian на Astra Linux прошел бы без изменений.
Возможно, я что-то не учитываю, но хотел поделиться идеей.
Stam_emg Автор
Спасибо, хорошая идея, Nix действительно решает похожую задачу.
Но с Nix я пока плотно не работал, но… вижу несколько нюансов.
1. Зафиксированный коммит nixpkgs замораживает и зависимости. Если взять nixpkgs того времени, когда Python 3.7 был актуален, вместе с ним приедет OpenSSL, zlib и всё остальное тех же лет со всеми уязвимостями, найденными с тех пор. Это ровно та проблема, с которой я начинал статью про замороженные официальные образы. Чтобы её решить, придётся переопределять OpenSSL через overlay и собирать из исходников. То есть делать то же самое, что у меня, только на языке Nix. Тут Nix ничем не хуже, но и магии нет.
2. Перенос «без изменений» — потому что closure тащит с собой всё, включая свою glibc. Технически это плюс. Но от Astra тогда остаётся только ядро:
системные библиотеки и сертифицированный OpenSSL не используются вообще. У меня в эксперименте рантайм работал поверх системной glibc Astra, и именно поэтому всплыли реальные отличия дистрибутива. Одно из них оказалось скрытой ошибкой, которая на Debian пока просто не проявилась.
3. Эксплуатация. У меня эти образы — база для пачки дочерних образов на обычном Debian, и команде проще читать Dockerfile, чем Nix-выражения. Для Astra в защищённом режиме /nix/store с неподписанными бинарниками — такая же, если не бОльшая, проблема, чем мои бинарники.
При этом сам подход «один раз описать окружение и воспроизводимо получать его где угодно» мне очень близок, и для разработческих окружений Nix, пожалуй, удобнее Docker.
Если у вас есть опыт держать на Nix длинный хвост EOL-версий с патчами безопасности, было бы интересно почитать. Спасибо еще раз!