Как все уже слышали, в конце мая Telegram зарелизил официальный MTProto Proxy (aka MTProxy) сервер, написанный на сях. В 2018 году без Docker мало куда, потому он сопровождается таким же «официальным» образом в формате zero-config. Все бы хорошо, но три «но» чуток испортили впечатление от релиза: image весит >130 Mb (там достаточно толстенький Debian, а не привычный Alpine), в силу «zero-config» оно не всегда удобно конфигурируется (только параметрами среды окружения) и ребята забыли, походу, выложить Dockerfile.



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 выполняет все приготовления для старта прокси.

Содержимое оригинального 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__.

Содержимое патча random_compat.patch
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)


  1. mwizard
    12.06.2018 13:22

    Посмотрите в сторону github.com/phusion/baseimage-docker. Перешли на него вместо alpine.


    1. neenik
      12.06.2018 14:01
      +2

      Превратили образ в виртуалку: ssh, syslog, инит, супервизор. И размер образа большой. Абсолютно противоположно идеологии Alpine.


      1. mwizard
        12.06.2018 14:45

        Лучшего всего идеологии 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.


        1. ProFfeSsoRr
          14.06.2018 05:59

          А зачем cron внутри контейнера? Он скорее где-то снаружи должен быть и самими контейнерами рулить, коли хочется его использовать.


    1. alexdoesh Автор
      12.06.2018 14:11
      +2

      Соглашусь с neenik, предложенный базовый образ не стоит сравнивать с Alpine точно. Да, некоторые фичи хороши и, возможно, в каких-то проектах даже удобны, но я сторонник модульности. Тот же dumb-init не всегда необходим, не говоря о SSH (это, имхо, идет в разрез с идеологией самого Docker'а).

      Но, спасибо за наводку, в любом случае, не сталкивался раньше с этой базой.


    1. Amomum
      12.06.2018 14:28

      А он такой же странный, как alpine?


      1. mwizard
        12.06.2018 14:29

        Нет, и динамически слинованные бинарники типа node.js работают из коробки.


        1. Amomum
          12.06.2018 14:33

          Это хорошо… жаль, что почти 100 метров весит.


      1. alexdoesh Автор
        12.06.2018 14:35

        А что странного с Alpine? Добиться такого объема без замены всего и вся на микро-альтернативы (которые действительно могут быть несовместимы с кучей софта) — было практически невозможно. Тут дело привычки, достаточно быстро Alpine становится другом.


        1. Amomum
          12.06.2018 14:41

          Это резонно, конечно, но как-то я не был готов к тому, что diff и date будут работать не так, как обычно. Подробностей уже не помню, к сожалению, помню только, как я страдал -_-


          1. alexdoesh Автор
            12.06.2018 15:23

            А еще grep (часть аргументов отсутствует, некоторые не соответствуют стандарту) и wget (не помню точно, что с ним). Потому если что-то нетривиальное — приходится ставить полную утилиту из какого-нибудь пакета.


        1. mwizard
          12.06.2018 14:47
          +1

          Друзья, у которых нет бинарной совместимости с другими дистрибутивами — не такие уже и друзья. Как пример, npm-пакеты node-sass или bcrypt невозможно просто «взять и установить» на node:10-alpine — нужно через apk поставить gcc и всю обвязку, и пересобрать их. Иначе жалуется на длинные отсутствующие символы.


          1. alexdoesh Автор
            12.06.2018 15:22
            +1

            Это ведь не совсем бинарная совместимость, дело в glibc/musl несовместимости. Тут как обычно есть tradeoff, сложно получить все и сразу. Если «пересборка» вам неудобна (это ведь docker, пересобрать не сложно), всегда можно установить glibc под alpine (этого пакета нет в официальных репозиториях, но он доступен на GitHub). Но даже старые добрые явисты начали пересобирать, емнип, вместо установки compatibility layer'а.

            Вы не подумайте, я не отстаиваю «правильность» Alpine, просто каждому инструменту свое место. Alpine хорош, когда важен объем, но да, есть нюансы.


  1. loskiq
    12.06.2018 15:24

    Не проще ли самому скомпилить?


    1. alexdoesh Автор
      12.06.2018 15:25
      +1

      Что вы имеете в виду? Всегда можно самому (и если будете делать это под Alpine — патч есть), оно и тут «самим скомпилено», просто в 2018 году без докера как-то неуютно уже. К удобствам быстро привыкаешь


  1. vsb
    12.06.2018 16:40

    А в чём проблема большого базового образа? Экономите место на диске? Статья интересная и познавательная, но по-мне, если софт не заводится на урезанном дистрибутиве, надо ставить полный и не морочить себе голову, чем патчить его. Завтра разработчики выкатят новую версию, в которой используют ещё что-нибудь из glibc, опять патчить придётся. Лучше бы разработчикам патч заслать в апстрим, чтобы новая версия без хаков собиралась на musl и заставить их тестировать на нём впредь, если уж хочется такого.


    1. alexdoesh Автор
      12.06.2018 16:47

      Разработчики не реагируют, к сожалению, на PR и обратную связь пока. Для них это не приоритетный проект совсем. А так согласен, конечно, в идеале им бы обернуть все директивами проверки на __GLIBC__ и учитывать в дальнейшем при разработке, что ни одним glibc живем. Будем надеяться, они обратят внимание на alpine-based образ и подтянут к себе.


  1. konchok
    12.06.2018 17:28
    +2

    Прокси этот крохотный, собрать его из исходников делов на минуту. На самых простецких VPS докер всё равно не запускается — каких-то модулей ядра нехватает.


    1. worldxaker
      12.06.2018 21:53
      +1

      на самой простенькой vps в vscale.io отлично работает докер. куда ещё проще то?


      1. konchok
        12.06.2018 22:11
        +1

        Есть куча VPS за $5 в год или вроди того, а у ваших даже тип виртуализации не указан — типа хавайте что дают. Подозреваю что дают KVM, на нём-то можно докрутить самому что хочешь.


        1. shifttstas
          12.06.2018 23:00
          +1

          а зачем брать VPS за 5$ на котором не работает докер если можно за 1$ где работает?


          1. neenik
            13.06.2018 00:22
            +1

            Подскажите, про какого хостера вы говорите (если это не arubacloud)?


            1. radist2s
              13.06.2018 03:22

              Вы имеете ввиду, что на арубе не очень со скоростью?


              1. MasMaX
                13.06.2018 15:19

                На арубе в облаках всё не очень :-)
                Ушел с их виртуалок на дедик к ним же. Было около 20 машин в облаке.


              1. neenik
                13.06.2018 15:23

                Нет, для таких маленьких виртуалок со скоростью там терпимо.
                Хотел ещё себе в кластер виртуалок в ДЦ, отличном от aruba. У самого aruba несколько ДЦ, но доступен для одно-евровых только один (IT-1). А сейчас они ещё и убрали возможность посмотреть, в каком ДЦ будет создана новая виртуалка.


        1. hashtet
          13.06.2018 12:53

          Ради интереса зашёл — нашёл за два клика про тип виртуализации(kvm).


      1. Murimonai
        13.06.2018 02:16
        +1

        Разворачивать телеграмм-прокси на VPS с датацентрами в Москве и Санкт-Петербурге…


        1. konchok
          13.06.2018 13:20

          Кстати вы зря смеетесь, просто надо дальше пробрасывать соединения через VPN за бугор. Зато никто такой прокси не заблокирует.


    1. namikiri
      12.06.2018 23:09

      Крайне, категорически согласен. Было бы здорово, если бы этот проект добавили в репозитории всяких Debian/Ubuntu, CentOS и иже с ними.

      Хотел было написать «или хотя-бы дали инструкцию по сборке с версиями библиотек», но они уже исправились и дописали readme.md.


  1. shifttstas
    12.06.2018 23:01
    +1

    PS Автор сделал таки PR в репозиторий прокси (патч с совместимостью)?