TL;DR Мы сделаем практически 1-в-1 официальный alpine-based docker image размером 5.94MB и положим его сюда (а Dockerfile сюда); попутно разберемся, как иногда можно подружить софт с Alpine с помощью кусачек и напильника, и чуток поиграемся размером, исключительно фана для.
Содержимое образа
Еще раз, из-за чего весь сыр-бор? Посмотрим, что представляет собой официальный образ командой history:
$ docker history --no-trunc --format "{{.Size}}\t{{.CreatedBy}}" telegrammessenger/proxy
Слои читаются снизу вверху, соответственно:
Самый толстый — это Debian Jessie, от которого унаследован оригинальный образ, имеено от него нам предстоит избавиться в первую очередь (alpine:3.6, для сравнения, весит 3.97MB); следом по габаритам идут curl и свежие сертификаты. Чтобы разобраться, что означают два остальных файла и каталог, заглянем внутрь с помощью команды run, подменив CMD на bash (так можно будет погулять по запущенному образу, познакомиться внимательнее, запускать те или иные фрагменты, скопировать чего полезного):
$ docker run -it --rm telegrammessenger/proxy /bin/bash
Теперь мы с легкостью можем восстановить картину происшествия — приблизительно так выглядел утерянный официальный Dockerfile:
FROM debian:jessie-20180312
RUN set -eux && apt-get update && apt-get install -y --no-install-recommends curl ca-certificates && rm -rf /var/lib/apt/lists/*
COPY ./mtproto-proxy /usr/local/bin
RUN mkdir /data
COPY ./secret/ /etc/telegram/
COPY ./run.sh /run.sh
CMD ["/bin/sh", "-c", "/bin/bash /run.sh"]
Где mtproto-proxy — скомпилированный сервер, папка secret содержит лишь файл hello-explorers-how-are-you-doing с ключом шифрования AES (см. команды сервера, там, кстати, официальная рекомендация получать ключ через API, но положили его таким макаром, вероятно, чтоб избежать ситуации, когда API тоже заблокирован), а run.sh выполняет все приготовления для старта прокси.
#!/bin/bash
if [ ! -z "$DEBUG" ]; then set -x; fi
mkdir /data 2>/dev/null >/dev/null
RANDOM=$(printf "%d" "0x$(head -c4 /dev/urandom | od -t x1 -An | tr -d ' ')")
if [ -z "$WORKERS" ]; then
WORKERS=2
fi
echo "####"
echo "#### Telegram Proxy"
echo "####"
echo
SECRET_CMD=""
if [ ! -z "$SECRET" ]; then
echo "[+] Using the explicitly passed secret: '$SECRET'."
elif [ -f /data/secret ]; then
SECRET="$(cat /data/secret)"
echo "[+] Using the secret in /data/secret: '$SECRET'."
else
if [[ ! -z "$SECRET_COUNT" ]]; then
if [[ ! ( "$SECRET_COUNT" -ge 1 && "$SECRET_COUNT" -le 16 ) ]]; then
echo "[F] Can generate between 1 and 16 secrets."
exit 5
fi
else
SECRET_COUNT="1"
fi
echo "[+] No secret passed. Will generate $SECRET_COUNT random ones."
SECRET="$(dd if=/dev/urandom bs=16 count=1 2>&1 | od -tx1 | head -n1 | tail -c +9 | tr -d ' ')"
for pass in $(seq 2 $SECRET_COUNT); do
SECRET="$SECRET,$(dd if=/dev/urandom bs=16 count=1 2>&1 | od -tx1 | head -n1 | tail -c +9 | tr -d ' ')"
done
fi
if echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}(,[0-9a-fA-F]{32}){,15}$'; then
SECRET="$(echo "$SECRET" | tr '[:upper:]' '[:lower:]')"
SECRET_CMD="-S $(echo "$SECRET" | sed 's/,/ -S /g')"
echo -- "$SECRET_CMD" > /data/secret_cmd
echo "$SECRET" > /data/secret
else
echo '[F] Bad secret format: should be 32 hex chars (for 16 bytes) for every secret; secrets should be comma-separated.'
exit 1
fi
if [ ! -z "$TAG" ]; then
echo "[+] Using the explicitly passed tag: '$TAG'."
fi
TAG_CMD=""
if [[ ! -z "$TAG" ]]; then
if echo "$TAG" | grep -qE '^[0-9a-fA-F]{32}$'; then
TAG="$(echo "$TAG" | tr '[:upper:]' '[:lower:]')"
TAG_CMD="-P $TAG"
else
echo '[!] Bad tag format: should be 32 hex chars (for 16 bytes).'
echo '[!] Continuing.'
fi
fi
curl -s https://core.telegram.org/getProxyConfig -o /etc/telegram/backend.conf || {
echo '[F] Cannot download proxy configuration from Telegram servers.'
exit 2
}
CONFIG=/etc/telegram/backend.conf
IP="$(curl -s -4 "https://digitalresistance.dog/myIp")"
INTERNAL_IP="$(ip -4 route get 8.8.8.8 | grep '^8\.8\.8\.8\s' | grep -Po 'src\s+\d+\.\d+\.\d+\.\d+' | awk '{print $2}')"
if [[ -z "$IP" ]]; then
echo "[F] Cannot determine external IP address."
exit 3
fi
if [[ -z "$INTERNAL_IP" ]]; then
echo "[F] Cannot determine internal IP address."
exit 4
fi
echo "[*] Final configuration:"
I=1
echo "$SECRET" | tr ',' '\n' | while read S; do
echo "[*] Secret $I: $S"
echo "[*] tg:// link for secret $I auto configuration: tg://proxy?server=${IP}&port=443&secret=${S}"
echo "[*] t.me link for secret $I: https://t.me/proxy?server=${IP}&port=443&secret=${S}"
I=$(($I+1))
done
[ ! -z "$TAG" ] && echo "[*] Tag: $TAG" || echo "[*] Tag: no tag"
echo "[*] External IP: $IP"
echo "[*] Make sure to fix the links in case you run the proxy on a different port."
echo
echo '[+] Starting proxy...'
sleep 1
exec /usr/local/bin/mtproto-proxy -p 2398 -H 443 -M "$WORKERS" -C 60000 --aes-pwd /etc/telegram/hello-explorers-how-are-you-doing -u root $CONFIG --allow-skip-dh --nat-info "$INTERNAL_IP:$IP" $SECRET_CMD $TAG_CMD
Сборка
Под CentOS 7 MTProxy на Хабре уже собирали, попробуем собрать образ под Alpine и сэкономить мегабайтов, эдак, 130 в результирующем docker image.
Отличительная особенность Alpine Linux — использование musl вместо glibc. Обе представляют из себя стандартные C библиотеки. Musl миниатюрен (в нем нет и пятой части «стандарта»), но объем и производительность (обещанная, по крайней мере), решают, когда речь идет о Docker. Да и ставить glibc на Alpine не является расово верным, дядька Jakub Jirutka не поймет, к примеру.
Собирать будем тоже в docker'е, чтоб изолировать зависимости и получить свободу для экспериментов, так что создадим новый Dockerfile:
FROM alpine:3.6
RUN apk add --no-cache git make gcc musl-dev linux-headers openssl-dev
RUN git clone --single-branch --depth 1 https://github.com/TelegramMessenger/MTProxy.git /mtproxy/sources
RUN cd /mtproxy/sources && make -j$(getconf _NPROCESSORS_ONLN)
Из зависимостей нам пригодится git (и не только для клонирования официального репозитория, make файл зашьет sha коммита в версию), make, gcc и заголовочные файлы (минимальный набор получен опытным путем). Клонируем только master branch глубиной в 1 коммит (нам точно не понадобится история). Ну и попробуем утилизировать все ресурсы хоста при компиляции с -j ключом. Умышленно разбил на большее количество слоев, чтоб получить удобное кэширование при пересборках (обычно их бывает немало).
Запускать будем командой build (находясь в директории с Dockerfile):
$ docker build -t mtproxy:test .
А вот и первая проблема:
In file included from ./net/net-connections.h:34:0,
from mtproto/mtproto-config.c:44:
./jobs/jobs.h:234:23: error: field 'rand_data' has incomplete type
struct drand48_data rand_data;
^~~~~~~~~
Собственно, все последующие будут связаны с ней же. Во-первых, для тех, кто не знаком с сями, компилятор на самом деле ругается на отсутствие декларации структуры drand48_data. Во-вторых, разработчики musl забили на потокобезопасные random-функции (с постфиксом _r) и на все что с ними связано (включая структуры). А разработчики Telegram, в свою очередь, не стали заморачиваться с компиляцией под системы, где random_r и его собратья не реализованы (во многих OS библиотеках можно встретить флаг HAVE_RANDOM_R или его произвольную + наличие или отсутствие этой группы функций обычно учитывается в автоконфигураторе).
Ну, теперь то точно будем ставить glibc? Нет! Мы скопируем из glibc по минимуму то, что нам нужно, и сделаем патч для исходников MTProxy.
Помимо проблем с random_r, мы хапнем проблему с функцией backtrace (execinfo.h), которая используется для вывода stack backtrace в случае исключительной ситуации: можно было бы попробовать заменить на имплементацию из libunwind, но оно того не стоит, потому вызов был обрамлен проверкой на __GLIBC__.
Index: jobs/jobs.h
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- jobs/jobs.h (revision cdd348294d86e74442bb29bd6767e48321259bec)
+++ jobs/jobs.h (date 1527996954000)
@@ -28,6 +28,8 @@
#include "net/net-msg.h"
#include "net/net-timers.h"
+#include "common/randr_compat.h"
+
#define __joblocked
#define __jobref
Index: common/server-functions.c
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- common/server-functions.c (revision cdd348294d86e74442bb29bd6767e48321259bec)
+++ common/server-functions.c (date 1527998325000)
@@ -35,7 +35,9 @@
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
+#ifdef __GLIBC__
#include <execinfo.h>
+#endif
#include <fcntl.h>
#include <getopt.h>
#include <grp.h>
@@ -168,6 +170,7 @@
}
void print_backtrace (void) {
+#ifdef __GLIBC__
void *buffer[64];
int nptrs = backtrace (buffer, 64);
kwrite (2, "\n------- Stack Backtrace -------\n", 33);
@@ -178,6 +181,7 @@
kwrite (2, s, strlen (s));
kwrite (2, "\n", 1);
}
+#endif
}
pthread_t debug_main_pthread_id;
Index: common/randr_compat.h
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- common/randr_compat.h (date 1527998264000)
+++ common/randr_compat.h (date 1527998264000)
@@ -0,0 +1,72 @@
+/*
+ The GNU C Library is free software. See the file COPYING.LIB for copying
+ conditions, and LICENSES for notices about a few contributions that require
+ these additional notices to be distributed. License copyright years may be
+ listed using range notation, e.g., 2000-2011, indicating that every year in
+ the range, inclusive, is a copyrightable year that would otherwise be listed
+ individually.
+*/
+
+#pragma once
+
+#include <endian.h>
+#include <pthread.h>
+
+struct drand48_data {
+ unsigned short int __x[3]; /* Current state. */
+ unsigned short int __old_x[3]; /* Old state. */
+ unsigned short int __c; /* Additive const. in congruential formula. */
+ unsigned short int __init; /* Flag for initializing. */
+ unsigned long long int __a; /* Factor in congruential formula. */
+};
+
+union ieee754_double
+{
+ double d;
+
+ /* This is the IEEE 754 double-precision format. */
+ struct
+ {
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned int negative:1;
+ unsigned int exponent:11;
+ /* Together these comprise the mantissa. */
+ unsigned int mantissa0:20;
+ unsigned int mantissa1:32;
+#endif /* Big endian. */
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ /* Together these comprise the mantissa. */
+ unsigned int mantissa1:32;
+ unsigned int mantissa0:20;
+ unsigned int exponent:11;
+ unsigned int negative:1;
+#endif /* Little endian. */
+ } ieee;
+
+ /* This format makes it easier to see if a NaN is a signalling NaN. */
+ struct
+ {
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned int negative:1;
+ unsigned int exponent:11;
+ unsigned int quiet_nan:1;
+ /* Together these comprise the mantissa. */
+ unsigned int mantissa0:19;
+ unsigned int mantissa1:32;
+#else
+ /* Together these comprise the mantissa. */
+ unsigned int mantissa1:32;
+ unsigned int mantissa0:19;
+ unsigned int quiet_nan:1;
+ unsigned int exponent:11;
+ unsigned int negative:1;
+#endif
+ } ieee_nan;
+};
+
+#define IEEE754_DOUBLE_BIAS 0x3ff /* Added to exponent. */
+
+int drand48_r (struct drand48_data *buffer, double *result);
+int lrand48_r (struct drand48_data *buffer, long int *result);
+int mrand48_r (struct drand48_data *buffer, long int *result);
+int srand48_r (long int seedval, struct drand48_data *buffer);
\ No newline at end of file
Index: Makefile
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- Makefile (revision cdd348294d86e74442bb29bd6767e48321259bec)
+++ Makefile (date 1527998107000)
@@ -40,6 +40,7 @@
DEPENDENCE_NORM := $(subst ${OBJ}/,${DEP}/,$(patsubst %.o,%.d,${OBJECTS}))
LIB_OBJS_NORMAL := + ${OBJ}/common/randr_compat.o ${OBJ}/common/crc32c.o ${OBJ}/common/pid.o ${OBJ}/common/sha1.o Index: common/randr_compat.c
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- common/randr_compat.c (date 1527998213000)
+++ common/randr_compat.c (date 1527998213000)
@@ -0,0 +1,120 @@
+/*
+ The GNU C Library is free software. See the file COPYING.LIB for copying
+ conditions, and LICENSES for notices about a few contributions that require
+ these additional notices to be distributed. License copyright years may be
+ listed using range notation, e.g., 2000-2011, indicating that every year in
+ the range, inclusive, is a copyrightable year that would otherwise be listed
+ individually.
+*/
+
+#include <stddef.h>
+#include "common/randr_compat.h"
+
+int __drand48_iterate (unsigned short int xsubi[3], struct drand48_data *buffer) {
+ uint64_t X;
+ uint64_t result;
+
+ /* Initialize buffer, if not yet done. */
+ if (!buffer->__init == 0)
+ {
+ buffer->__a = 0x5deece66dull;
+ buffer->__c = 0xb;
+ buffer->__init = 1;
+ }
+
+ /* Do the real work. We choose a data type which contains at least
+ 48 bits. Because we compute the modulus it does not care how
+ many bits really are computed. */
+
+ X = (uint64_t) xsubi[2] << 32 | (uint32_t) xsubi[1] << 16 | xsubi[0];
+
+ result = X * buffer->__a + buffer->__c;
+
+ xsubi[0] = result & 0xffff;
+ xsubi[1] = (result >> 16) & 0xffff;
+ xsubi[2] = (result >> 32) & 0xffff;
+
+ return 0;
+}
+
+int __erand48_r (unsigned short int xsubi[3], struct drand48_data *buffer, double *result) {
+ union ieee754_double temp;
+
+ /* Compute next state. */
+ if (__drand48_iterate (xsubi, buffer) < 0)
+ return -1;
+
+ /* Construct a positive double with the 48 random bits distributed over
+ its fractional part so the resulting FP number is [0.0,1.0). */
+
+ temp.ieee.negative = 0;
+ temp.ieee.exponent = IEEE754_DOUBLE_BIAS;
+ temp.ieee.mantissa0 = (xsubi[2] << 4) | (xsubi[1] >> 12);
+ temp.ieee.mantissa1 = ((xsubi[1] & 0xfff) << 20) | (xsubi[0] << 4);
+
+ /* Please note the lower 4 bits of mantissa1 are always 0. */
+ *result = temp.d - 1.0;
+
+ return 0;
+}
+
+int __nrand48_r (unsigned short int xsubi[3], struct drand48_data *buffer, long int *result) {
+ /* Compute next state. */
+ if (__drand48_iterate (xsubi, buffer) < 0)
+ return -1;
+
+ /* Store the result. */
+ if (sizeof (unsigned short int) == 2)
+ *result = xsubi[2] << 15 | xsubi[1] >> 1;
+ else
+ *result = xsubi[2] >> 1;
+
+ return 0;
+}
+
+int __jrand48_r (unsigned short int xsubi[3], struct drand48_data *buffer, long int *result) {
+ /* Compute next state. */
+ if (__drand48_iterate (xsubi, buffer) < 0)
+ return -1;
+
+ /* Store the result. */
+ *result = (int32_t) ((xsubi[2] << 16) | xsubi[1]);
+
+ return 0;
+}
+
+int drand48_r (struct drand48_data *buffer, double *result) {
+ return __erand48_r (buffer->__x, buffer, result);
+}
+
+int lrand48_r (struct drand48_data *buffer, long int *result) {
+ /* Be generous for the arguments, detect some errors. */
+ if (buffer == NULL)
+ return -1;
+
+ return __nrand48_r (buffer->__x, buffer, result);
+}
+
+int mrand48_r (struct drand48_data *buffer, long int *result) {
+ /* Be generous for the arguments, detect some errors. */
+ if (buffer == NULL)
+ return -1;
+
+ return __jrand48_r (buffer->__x, buffer, result);
+}
+
+int srand48_r (long int seedval, struct drand48_data *buffer) {
+ /* The standards say we only have 32 bits. */
+ if (sizeof (long int) > 4)
+ seedval &= 0xffffffffl;
+
+ buffer->__x[2] = seedval >> 16;
+ buffer->__x[1] = seedval & 0xffffl;
+ buffer->__x[0] = 0x330e;
+
+ buffer->__a = 0x5deece66dull;
+ buffer->__c = 0xb;
+ buffer->__init = 1;
+
+ return 0;
+}
\ No newline at end of file
Положим его в папку ./patches и немного модифицируем наш Dockerfile, чтоб применять патч «на лету»:
FROM alpine:3.6
COPY ./patches /mtproxy/patches
RUN apk add --no-cache --virtual .build-deps git make gcc musl-dev linux-headers openssl-dev && git clone --single-branch --depth 1 https://github.com/TelegramMessenger/MTProxy.git /mtproxy/sources && cd /mtproxy/sources && patch -p0 -i /mtproxy/patches/randr_compat.patch && make -j$(getconf _NPROCESSORS_ONLN) && cp /mtproxy/sources/objs/bin/mtproto-proxy /mtproxy/ && rm -rf /mtproxy/{sources,patches} && apk add --no-cache --virtual .rundeps libcrypto1.0 && apk del .build-deps
Теперь собранный бинарник mtproto-proxy как минимум запускается, и мы можем двинуться дальше.
Оформление
Пришло время превратить оригинальный run.sh в docker-entrypoint.sh. На мой взгляд, это логично, когда «обязательная обвязка» уходит в ENTRYPOINT (его всегда можно перегрузить снаружи), а аргументы запуска докеризованного приложения по максимуму укладываются в CMD (+переменные среды окружения в качестве дублера).
Мы могли бы установить в наш альпийский образ bash и полноценный grep (поясню далее), чтоб избежать головной боли и использовать оригинальный код как есть, но это до безобразия раздует наш миниатюрный образ, потому будем растить настоящий, мать его, бонсай.
Начнем с шебанга, заменим
#!/bin/bash
на #!/bin/sh
. Дефолтный для alpine ash способен переварить практически весь синтаксис bash'а «as is», но с одной проблемой мы все же столкнемся — по неведомым причинам он отказался принять parenthesis в одном из условий, потому развернем его, инвертируя логику сравнения:Теперь нас ждут разборки с grep'ом, который в busybox поставке немного отличается от привычного (и, кстати, значительно медленнее, имейте в виду в своих проектах). Во-первых, он не понимает выражения
{,15}
, придется явно указать {0,15}
. Во-вторых, не поддерживает флаг -P
(perl style), но спокойно переваривает выражение при включенном extended (-E).У нас в зависимостях остается лишь curl (нет никакого смысла заменять его на wget из busybox'а) и libcrypto (его достаточно, непосредственно openssl в этой сборке совсем не требуется).
Пару лет назад в Docker появился прекрасный multi-stage build, он идеально подходит, к примеру, для Go приложений или для ситуаций, когда сборка сложна и проще передавать артефакты от образа к образу, чем делать финальную очистку. Для посадки нашего бонсая мы им воспользуемся, это позволит немного сэкономить.
FROM alpine:3.6
# Этот образ будет будет использован только для сборки (кэш сохранится)
RUN apk add --no-cache --virtual .build-deps # ... пропустим, покажем последний аккорд
&& make -j$(getconf _NPROCESSORS_ONLN)
FROM alpine:3.6
# А это финальный, чистенький образ, ничего лишнего
WORKDIR /mtproxy
COPY --from=0 /mtproxy/sources/objs/bin/mtproto-proxy .
# Просто скопируем артефакт из первого образа в чистую среду
# Ну и выполним дальше специфичные для финального образа процедуры
Бонсай должен быть бонсаем — избавимся от установки libcrypto. При сборке нам нужны были заголовочные файлы из пакета openssl-dev, который в зависимостях подтянет libcrypto и наш исполняемый файл окажется сориентирован на использование libcrypto.so.1.0.0. А ведь это единственная зависимость, к тому же, она предустановлена в Alpine (в версии 3.6 это libcrypto.so.41, 3.7 — libcrypto.so.42, лежит в /lib/). Меня сейчас отругают, это не самый надежный способ, но оно того стоит и мы все-таки добавим симлинк на имеющуюся версию (если у вас есть способ лучше, с удовольствием приму PR).
Финальные штрихи и результат:
Docker Hub
GitHub
Буду признателен любым советам и контрибуциям.
Комментарии (30)
vsb
12.06.2018 16:40А в чём проблема большого базового образа? Экономите место на диске? Статья интересная и познавательная, но по-мне, если софт не заводится на урезанном дистрибутиве, надо ставить полный и не морочить себе голову, чем патчить его. Завтра разработчики выкатят новую версию, в которой используют ещё что-нибудь из glibc, опять патчить придётся. Лучше бы разработчикам патч заслать в апстрим, чтобы новая версия без хаков собиралась на musl и заставить их тестировать на нём впредь, если уж хочется такого.
alexdoesh Автор
12.06.2018 16:47Разработчики не реагируют, к сожалению, на PR и обратную связь пока. Для них это не приоритетный проект совсем. А так согласен, конечно, в идеале им бы обернуть все директивами проверки на __GLIBC__ и учитывать в дальнейшем при разработке, что ни одним glibc живем. Будем надеяться, они обратят внимание на alpine-based образ и подтянут к себе.
konchok
12.06.2018 17:28+2Прокси этот крохотный, собрать его из исходников делов на минуту. На самых простецких VPS докер всё равно не запускается — каких-то модулей ядра нехватает.
worldxaker
12.06.2018 21:53+1на самой простенькой vps в vscale.io отлично работает докер. куда ещё проще то?
konchok
12.06.2018 22:11+1Есть куча VPS за $5 в год или вроди того, а у ваших даже тип виртуализации не указан — типа хавайте что дают. Подозреваю что дают KVM, на нём-то можно докрутить самому что хочешь.
shifttstas
12.06.2018 23:00+1а зачем брать VPS за 5$ на котором не работает докер если можно за 1$ где работает?
neenik
13.06.2018 00:22+1Подскажите, про какого хостера вы говорите (если это не arubacloud)?
radist2s
13.06.2018 03:22Вы имеете ввиду, что на арубе не очень со скоростью?
MasMaX
13.06.2018 15:19На арубе в облаках всё не очень :-)
Ушел с их виртуалок на дедик к ним же. Было около 20 машин в облаке.
neenik
13.06.2018 15:23Нет, для таких маленьких виртуалок со скоростью там терпимо.
Хотел ещё себе в кластер виртуалок в ДЦ, отличном от aruba. У самого aruba несколько ДЦ, но доступен для одно-евровых только один (IT-1). А сейчас они ещё и убрали возможность посмотреть, в каком ДЦ будет создана новая виртуалка.
namikiri
12.06.2018 23:09Крайне, категорически согласен. Было бы здорово, если бы этот проект добавили в репозитории всяких Debian/Ubuntu, CentOS и иже с ними.
Хотел было написать «или хотя-бы дали инструкцию по сборке с версиями библиотек», но они уже исправились и дописали readme.md.
mwizard
Посмотрите в сторону github.com/phusion/baseimage-docker. Перешли на него вместо alpine.
neenik
Превратили образ в виртуалку: ssh, syslog, инит, супервизор. И размер образа большой. Абсолютно противоположно идеологии Alpine.
mwizard
Лучшего всего идеологии Alpine соответствует `FROM scratch`, тем не менее, не так много желающих следовать этому пути. Одиночные образы — да, но поставить сборку на конвейер, и так, чтобы ничего не ломалось… Затраты времени на создание грамотного сборщика артефактов и всех их динамически слинкованных зависимостей в таком случае превысят стоимость сэкономленного дискового пространства.
Достоинства корректного init-процесса описываются во многих местах, включая README.md в репозитории этого образа — если ваше приложение создает субпроцессы для любых целей (сразу же в голову приходит imagemagick и его форки), вы обязаны собирать мертвых детей их детей. Делает ли это ваше приложение? Если нет, то ваш контейнер бездумно течет памятью. Если да, вы реализовали init самостоятельно, возможно, даже сделали это правильно, с обработкой всех возможных сигналов, но зачем?
cron — что плохого в cron? Предпочитаете, опять же, реализовать свой собственный cron, с преферансом и библиотекаршами, на js/python/ruby/erlang/php/etc?
ssh — мне ни разу не понадобился, и я отключил его при сборке. Соглашусь с вами в том, что ssh внутри контейнера не нужен, т.к. есть docker exec, а сам контейнер должен быть максимально эфемерным и stateless.
syslog — если у вас логи очень простые и syslog вам не нужен, это замечательно. В случае нашего проекта, у нас json-логи, мы добавляем к каждой записи кучу дополнительной информации об окружении, плюс в зависимости от окружения (dev / staging) мы можем запустить все наши сервисы в одном контейнере, чтобы упростить отладку. Неизобретение своих велосипедов, а использование стандартного клиента для syslog сильно облегчает жизнь тому, кто потом читает агрегированный JSON-лог. Можно, конечно, было бы изобрести свой сервис логгирования, и скармливать логи в него удаленно, но зачем дублировать существующую инфраструктуру docker logs?
Что касается супервизора — я видел в куче других проектов использование nodemon, supervisor, foreman и т.д. для того, чтобы переподнять проект, если он упал. В нашем проекте мы используем внешнюю оркестрацию (через docker swarm), а на этапе разработки позволяем одному крешнувшемуся процессу утянуть за собой весь контейнер (чтобы нельзя было пропустить баг). Кому-то будет удобно и полезно, если организовывать контейнер так, чтобы внутри него был один логический микросервис, с кучей других процессов, будь то ntp, или init, или syslog, которые являются «деталью реализации».
Я же не предлагаю в этот образ запихивать весь проект, для такой идеологии уже есть vagrant.
ProFfeSsoRr
А зачем cron внутри контейнера? Он скорее где-то снаружи должен быть и самими контейнерами рулить, коли хочется его использовать.
alexdoesh Автор
Соглашусь с neenik, предложенный базовый образ не стоит сравнивать с Alpine точно. Да, некоторые фичи хороши и, возможно, в каких-то проектах даже удобны, но я сторонник модульности. Тот же dumb-init не всегда необходим, не говоря о SSH (это, имхо, идет в разрез с идеологией самого Docker'а).
Но, спасибо за наводку, в любом случае, не сталкивался раньше с этой базой.
Amomum
А он такой же странный, как alpine?
mwizard
Нет, и динамически слинованные бинарники типа node.js работают из коробки.
Amomum
Это хорошо… жаль, что почти 100 метров весит.
alexdoesh Автор
А что странного с Alpine? Добиться такого объема без замены всего и вся на микро-альтернативы (которые действительно могут быть несовместимы с кучей софта) — было практически невозможно. Тут дело привычки, достаточно быстро Alpine становится другом.
Amomum
Это резонно, конечно, но как-то я не был готов к тому, что diff и date будут работать не так, как обычно. Подробностей уже не помню, к сожалению, помню только, как я страдал -_-
alexdoesh Автор
А еще grep (часть аргументов отсутствует, некоторые не соответствуют стандарту) и wget (не помню точно, что с ним). Потому если что-то нетривиальное — приходится ставить полную утилиту из какого-нибудь пакета.
mwizard
Друзья, у которых нет бинарной совместимости с другими дистрибутивами — не такие уже и друзья. Как пример, npm-пакеты node-sass или bcrypt невозможно просто «взять и установить» на node:10-alpine — нужно через apk поставить gcc и всю обвязку, и пересобрать их. Иначе жалуется на длинные отсутствующие символы.
alexdoesh Автор
Это ведь не совсем бинарная совместимость, дело в glibc/musl несовместимости. Тут как обычно есть tradeoff, сложно получить все и сразу. Если «пересборка» вам неудобна (это ведь docker, пересобрать не сложно), всегда можно установить glibc под alpine (этого пакета нет в официальных репозиториях, но он доступен на GitHub). Но даже старые добрые явисты начали пересобирать, емнип, вместо установки compatibility layer'а.
Вы не подумайте, я не отстаиваю «правильность» Alpine, просто каждому инструменту свое место. Alpine хорош, когда важен объем, но да, есть нюансы.