Предисловие
Так сложилось, что мне приходится работать над большим количеством сайтов, задачи решать так же разные - от настроек сервера до "сверстать форму". И вот на одном из проектов возникла задача - обновиться до актуальной версии php (8.1 на момент написания), обновить до актуальной версии CMS (1C Bitrix), ну и в целом, "довести до ума".
Поскольку проект оброс значительным количеством функционала, не связанного с сайтом напрямую (инкрементальные и полные бэкапы по расписанию с выгрузкой в облако, составление словарей, синхронизации с разными поставщиками), а работы ведутся в 3 окружениях (локально, тестовая площадка и продакшн сайт), то я решил, что это будет хорошей возможностью перенести всю инфраструктуру на контейнеры Docker.
Поскольку технология уже устоявшаяся, то ожидалось, что найдется готовый шаблон сервера "из коробки", который подойдет под наши нужды. Но поискав, не удалось найти полноценного решения - везде были какие-то нюансы, из-за которых решение не подходило. В результате был собран собственный сервер для сайта на 1С Битрикс. После чего из сервера было вырезано все, что связано с этой CMS и теперь он может использоваться под другие проекты без ограничений.
Код доступен на github.
Компоненты сервера
Для полноценной работы сервера нам нужны следующие компоненты:
база данных (MySQL);
PHP;
NGINX;
прокси для отправки почты (msmtp);
composer;
letsencrypt сертификаты;
резервное копирование и восстановление;
опционально - облако для хранения бэкапов.
Так же нам нужно по расписанию запускать разные действия. Для этого будет использоваться crontab на хосте, а не в контейнерах.
Перед началом работ
На сервере нам понадобится docker-compose. Инструкции:
подготовка сервера;
установка docker;
установка docker-compose.
Так же нам нужны будут доступы к smtp почтового сервиса и s3 хранилища для бэкапов (опционально).
По поводу gmail smtp
Google сообщил, что с июня 2022 года приостанавливает доступ небезопасных приложений (с авторизацией только по паролю аккаунта). Чтобы получить возможность использовать gmail smtp, надо в настройках аккаунта включить двухфакторную авторизацию, создать отдельный пароль авторизации для нашего сайта и использовать его. Подробных инструкций достаточно.
Сервисы и окружения
Для гибкости в настройке сервера создаем 4 отдельных файла compose.yml:
compose-app.yml - основные сервисы нашего приложения (база данных, php, nginx, composer);
compose-https.yml - для работы сайта по протоколу https. Включает в себя certbot, а так же правила перенаправления с http на https для nginx;
compose-cloud.yml - для хранения бэкапов в холодном хранилище;
compose-production.yml - переопределяет правила рестарта для всех контейнеров.
compose-app.yml
version: '3'
services:
db:
image: mysql
container_name: database
restart: unless-stopped
tty: true
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_USER_PASSWORD}
volumes:
- ./.backups:/var/www/.backups
- ./.docker/mysql/my.cnf:/etc/mysql/my.cnf
- database:/var/lib/mysql
networks:
- backend
app:
image: php:8.1-fpm
container_name: application
build:
context: .
dockerfile: Dockerfile
args:
GID: ${SYSTEM_GROUP_ID}
UID: ${SYSTEM_USER_ID}
SMTP_HOST: ${MAIL_SMTP_HOST}
SMTP_PORT: ${MAIL_SMTP_PORT}
SMTP_EMAIL: ${MAIL_SMTP_USER}
SMTP_PASSWORD: ${MAIL_SMTP_PASSWORD}
restart: unless-stopped
tty: true
working_dir: /var/www/app
volumes:
- ./app:/var/www/app
- ./log:/var/www/log
- ./.docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
networks:
- backend
links:
- "webserver:${APP_NAME}"
composer:
build:
context: .
image: composer
container_name: composer
working_dir: /var/www/app
command: "composer install"
restart: "no"
depends_on:
- app
volumes:
- ./app:/var/www/app
webserver:
image: nginx:stable-alpine
container_name: webserver
restart: unless-stopped
tty: true
ports:
- "80:80"
- "443:443"
volumes:
- ./app/public:/var/www/app/public
- ./log:/var/www/log
- ./.docker/nginx/default.conf:/etc/nginx/includes/default.conf
- ./.docker/nginx/templates/http.conf.template:/etc/nginx/templates/website.conf.template
environment:
- APP_NAME=${APP_NAME}
networks:
- frontend
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
volumes:
database:
compose-https.yml
version: '3'
services:
webserver:
volumes:
- ./.docker/certbot/conf:/etc/letsencrypt
- ./.docker/certbot/www:/var/www/.docker/certbot/www
- ./.docker/nginx/templates/https.conf.template:/etc/nginx/templates/website.conf.template
certbot:
image: certbot/certbot
container_name: certbot
restart: "no"
volumes:
- ./log/letsencrypt:/var/www/log/letsencrypt
- ./.docker/certbot/conf:/etc/letsencrypt
- ./.docker/certbot/www:/var/www/.docker/certbot/www
compose-cloud.yml
version: '3'
services:
cloudStorage:
image: efrecon/s3fs
container_name: cloudStorage
restart: unless-stopped
cap_add:
- SYS_ADMIN
security_opt:
- 'apparmor:unconfined'
devices:
- /dev/fuse
environment:
AWS_S3_BUCKET: ${AWS_S3_BUCKET}
AWS_S3_ACCESS_KEY_ID: ${AWS_S3_ACCESS_KEY_ID}
AWS_S3_SECRET_ACCESS_KEY: ${AWS_S3_SECRET_ACCESS_KEY}
AWS_S3_URL: ${AWS_S3_URL}
AWS_S3_MOUNT: '/opt/s3fs/bucket'
S3FS_ARGS: -o use_path_request_style
GID: ${SYSTEM_GROUP_ID}
UID: ${SYSTEM_USER_ID}
volumes:
- ${AWS_S3_LOCAL_MOUNT_POINT}:/opt/s3fs/bucket:rshared
compose-production.yml
version: '3'
services:
db:
restart: always
app:
restart: always
webserver:
restart: always
cloudStorage:
restart: always
И определяем настройки окружения в файле .env
.env
COMPOSE_FILE=compose-app.yml:compose-cloud.yml:compose-https.yml:compose-production.yml
SYSTEM_GROUP_ID=1000
SYSTEM_USER_ID=1000
APP_NAME=example.local
ADMINISTRATOR_EMAIL=example@gmail.com
DB_HOST=db
DB_DATABASE=example_db
DB_USER=example
DB_USER_PASSWORD=example
DB_ROOT_PASSWORD=example
AWS_S3_URL=http://storage.example.net
AWS_S3_BUCKET=storage
AWS_S3_ACCESS_KEY_ID=#YOU_KEY#
AWS_S3_SECRET_ACCESS_KEY=#YOU_KEY_SECRET#
AWS_S3_LOCAL_MOUNT_POINT=/mnt/s3backups
MAIL_SMTP_HOST=smtp.gmail.com
MAIL_SMTP_PORT=587
MAIL_SMTP_USER=example@gmail.com
MAIL_SMTP_PASSWORD=example
В зависимости от того, какой набор сервисов нужен нам в конкретном окружении - указываем в переменной COMPOSE_FILE набор compose-*.yml файлов
В каталоге .docker/ храним настройки для всех сервисов, которые используются в приложении. Тут стоит отметить 2 из них:
Для nginx мы используем файл с правилами .docker/nginx/default.conf и два шаблона (.docker/nginx/templates/http.conf.template и .docker/nginx/templates/https.conf.template). В зависимости от того, по какому протоколу работаем - будут использованы соответствующие настройки nginx. О шаблонах подробно сказано на странице образа nginx;
Для msmtp в файле .docker/msmtp/msmtp мы указываем заплатки вида #PASSWORD#, которые будут заменены при построении образа.
.docker/msmtp/msmtprc
# Set default values for all following accounts.
defaults
auth on
tls on
logfile /var/www/log/msmtp/msmtp.log
timeout 5
account docker
host #HOST#
port #PORT#
from #EMAIL#
user #EMAIL#
password #PASSWORD#
# Set a default account
account default : docker
Создаем файл Dockerfile, в котором укажем особенности сборки и, как говорилось ранее, для msmtp задаем параметры подключения из переменных окружения:
Dockerfile
FROM php:8.1-fpm
ARG GID
ARG UID
ARG SMTP_HOST
ARG SMTP_PORT
ARG SMTP_EMAIL
ARG SMTP_PASSWORD
USER root
WORKDIR /var/www
RUN apt-get update -y \
&& apt-get autoremove -y \
&& apt-get -y --no-install-recommends \
msmtp \
zip \
unzip \
&& rm -rf /var/lib/apt/lists/*
COPY ./.docker/msmtp/msmtprc /etc/msmtprc
RUN sed -i "s/#HOST#/$SMTP_HOST/" /etc/msmtprc \
&& sed -i "s/#PORT#/$SMTP_PORT/" /etc/msmtprc \
&& sed -i "s/#EMAIL#/$SMTP_EMAIL/" /etc/msmtprc \
&& sed -i "s/#PASSWORD#/$SMTP_PASSWORD/" /etc/msmtprc
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN getent group www || groupadd -g $GID www \
&& getent passwd $UID || useradd -u $UID -m -s /bin/bash -g www www
USER www
EXPOSE 9000
CMD ["php-fpm"]
Резервное копирование
Бэкап состоит из двух частей: архив с файлами и дамп базы данных. Хранить их мы можем локально, либо отправлять в облако. Для формирования используем скрипт cgi-bin/create-backup.sh.
Для восстановления - cgi-bin/restore-backup.sh соответственно. Если у нас подключено облачное хранилище - то предложим восстанавливать из него:
create-backup.sh
#!/bin/bash
BASEDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../" &> /dev/null && pwd)
source "$BASEDIR/.env"
cd "$BASEDIR/"
# If run script with --local, then don't send backup to remote storage
moveToCloud="Y"
while [ $# -gt 0 ] ; do
case $1 in
--local) moveToCloud="N";;
esac
shift
done
# If backups storage is not mounted, then anyway store backups local
if ! [[ $COMPOSE_FILE == *"compose-cloud.yml"* ]]; then
moveToCloud="N"
fi
# Current date, 2022-01-25_16-10
timestamp=`date +"%Y-%m-%d_%H-%M"`
backups_local_folder="$BASEDIR/.backups/local"
backups_cloud_folder="$AWS_S3_LOCAL_MOUNT_POINT"
# Creating local folder for backups
mkdir -p "$backups_local_folder"
# Creating backup of application
tar \
--exclude='vendor' \
-czvf $backups_local_folder/"$timestamp"_app.tar.gz \
-C $BASEDIR "app"
# Creating backup of database
docker exec database sh -c "exec mysqldump -u root -h $DB_HOST -p$DB_ROOT_PASSWORD $DB_DATABASE" > $backups_local_folder/"$timestamp"_database.sql
gzip $backups_local_folder/"$timestamp"_database.sql
# If required, then move current backup to cloud storage
if [ $moveToCloud == "Y" ]; then
mv $backups_local_folder/"$timestamp"_database.sql.gz $backups_cloud_folder/"$timestamp"_database.sql.gz
mv $backups_local_folder/"$timestamp"_app.tar.gz $backups_cloud_folder/"$timestamp"_app.tar.gz
fi
# If we already moved backup to cloud, then remove old backups (older than 30 days) from cloud storage
if [ $moveToCloud == "Y" ]; then
/usr/bin/find $backups_cloud_folder/ -type f -mtime +30 -exec rm {} \;
fi
restore-backup.sh
#!/bin/bash
BASEDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../" &> /dev/null && pwd)
source "$BASEDIR/.env"
cd "$BASEDIR/"
backupsDestination="$BASEDIR/.backups/local"
# If backups storage is mounted, ask, from where will restore backups
if [[ $COMPOSE_FILE == *"compose-cloud.yml"* ]]; then
while true
do
reset
echo "Select backups destination:"
echo "1. Local;"
echo "2. Cloud;"
echo "---------"
echo "0. Exit"
read -r choice
case $choice in
"0")
exit
;;
"1")
break
;;
"2")
backupsDestination="$AWS_S3_LOCAL_MOUNT_POINT"
break
;;
*)
;;
esac
done
fi
reset
# Select backup for restore
echo "Available backups:"
find "$backupsDestination"/*.gz -printf "%f\n"
echo "------------"
echo "Enter backup path:"
read -i "$backupsDestination"/ -e backup_name
if ! [ -f "$backup_name" ]; then
echo "Wrong backup path."
exit 1
fi
backup_mode="unknown"
if [[ $backup_name == *"app.tar.gz"* ]]; then
backup_mode="app"
elif [[ $backup_name == *"database.sql.gz"* ]]; then
backup_mode="database"
fi
if [ $backup_mode == "unknown" ]; then
echo "Unknown backup type"
exit 1
fi
reset
if [ $backup_mode == "app" ]; then
mkdir -p "$BASEDIR"/.backups/tmp
cp "$backup_name" "$BASEDIR"/.backups/tmp/app_tmp.tar.gz
tar -xvf "$BASEDIR"/.backups/tmp/app_tmp.tar.gz -C "$BASEDIR"
rm -rf "$BASEDIR"/.backups/tmp
fi
if [ $backup_mode == "database" ]; then
mkdir -p "$BASEDIR"/.backups/tmp
cp "$backup_name" "$BASEDIR"/.backups/tmp/database_tmp.sql.gz
gunzip "$BASEDIR"/.backups/tmp/database_tmp.sql.gz
if ! [ -f "$BASEDIR"/.backups/tmp/database_tmp.sql ]; then
echo "Error in database unpack process"
exit 1
fi
docker-compose exec db bash -c "exec mysql -u root -p$DB_ROOT_PASSWORD $DB_DATABASE < /var/www/.backups/tmp/database_tmp.sql"
rm -rf "$BASEDIR"/.backups/tmp
fi
Crontab
Запуск по расписанию делаем на стороне хоста. Для инициализации используется файл cgi-bin/prepare-crontab.sh. В ходе выполнения скрипт собирает все файлы из каталога .crontab, заменяет в них путь к приложению #APP_PATH# на актуальный, и вносит их в crontab на хосте.
prepare-crontab.sh
#!/bin/bash
BASEDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../" &> /dev/null && pwd)
# Load environment variables
source "$BASEDIR"/.env
# Create temporary directory
mkdir -p "$BASEDIR"/.crontab_tmp/
# Copy all crontab files to temporary directory
cp "$BASEDIR"/.crontab/* "$BASEDIR"/.crontab_tmp/
# Set actual app path in crontab files
find "$BASEDIR"/.crontab_tmp/ -name "*.cron" -exec sed -i "s|#APP_PATH#|$BASEDIR|g" {} +
# Set crontab
if [[ $COMPOSE_FILE == *"compose-https.yml"* ]]; then
find "$BASEDIR"/.crontab_tmp/ -name '*.cron' -exec cat {} \; | crontab -
else
find "$BASEDIR"/.crontab_tmp/ -name '*.cron' -not -name 'certbot-renew.cron' -exec cat {} \; | crontab -
fi
# Remove temporary directory
rm -rf "$BASEDIR"/.crontab_tmp/
Certbot
Если https в рамках данного окружения не нужен - то этот шаг пропускаем.
Для получения ssl сертификатов используем certbot. Но тут есть одна особенность - для подтверждения владения доменом нам нужно запустить nginx, но без сертификатов он не запустится. Получается замкнутый круг. Для решения используем скрипт cgi-bin/prepare-certbot.sh, который создает сертификаты-заглушки, запускает nginx, запрашивает актуальные сертификаты, устанавливает их и перезапускает nginx.
Для обновления сертификатов создадим файл cgi-bin/certbot-renew.sh, который будем запускать по расписанию.
prepare-certbot.sh
#!/bin/bash
BASEDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../" &> /dev/null && pwd)
source "$BASEDIR/.env"
cd "$BASEDIR/"
if ! [ -x "$(command -v docker-compose)" ]; then
echo 'Error: docker-compose is not installed.' >&2
exit 1
fi
domains=($APP_NAME www.$APP_NAME)
rsa_key_size=4096
data_path="$BASEDIR/.docker/certbot"
email=$ADMINISTRATOR_EMAIL
staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits
if [ -d "$data_path" ]; then
read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision
if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
exit
fi
fi
if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then
echo "### Downloading recommended TLS parameters ..."
mkdir -p "$data_path/conf"
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf"
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem"
echo
fi
echo "### Creating dummy certificate for $domains ..."
path="/etc/letsencrypt/live/$domains"
mkdir -p "$data_path/conf/live/$domains"
docker-compose run --rm --entrypoint "\
openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\
-keyout '$path/privkey.pem' \
-out '$path/fullchain.pem' \
-subj '/CN=localhost'" certbot
echo
echo "### Starting nginx ..."
docker-compose up --force-recreate -d webserver
echo
echo "### Deleting dummy certificate for $domains ..."
docker-compose run --rm --entrypoint "\
rm -Rf /etc/letsencrypt/live/$domains && \
rm -Rf /etc/letsencrypt/archive/$domains && \
rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot
echo
echo "### Requesting Let's Encrypt certificate for $domains ..."
domain_args=""
for domain in "${domains[@]}"; do
domain_args="$domain_args -d $domain"
done
case "$email" in
"") email_arg="--register-unsafely-without-email" ;;
*) email_arg="--email $email" ;;
esac
if [ $staging != "0" ]; then staging_arg="--staging"; fi
docker-compose run --rm --entrypoint "\
certbot certonly --webroot -w /var/www/.docker/certbot/www \
$staging_arg \
$email_arg \
$domain_args \
--rsa-key-size $rsa_key_size \
--agree-tos \
--force-renewal" certbot
echo
echo "### Reloading nginx ..."
docker-compose exec webserver nginx -s reload
certbot-renew.sh
#!/bin/bash
BASEDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../" &> /dev/null && pwd)
cd "$BASEDIR/"
docker-compose run --rm certbot renew && docker-compose kill -s SIGHUP webserver
docker system prune -af
На этом этапе сайт доступен, и с ним можно продолжать работы.
Пошаговый процесс установки и описание переменных доступны на github.
Комментарии (21)
php7
12.06.2022 11:14-3К mysql можно достучаться из мира? По какому адресу?
Можно ли выполнить из командной строки php-скрипт? Как?
Можно ли перезапускать php через systemctl?
Можно ли подключиться к контейнеру по ssh?
undersunn Автор
12.06.2022 12:34+4К mysql достучаться - нельзя, поскольку на хосте открыты только 22, 80, 443 порты. Если есть такая необходимость - то надо на хосте открыть порт 3306, а в compose-app.yml в сервисе database в секции "ports" указать:
ports: - "3306:3306"
Выполнить скрипт из командной строки - можно. Поскольку PHP у нас только в контейнере application, то запускать php скрипт мы будем именно через этот контейнер. Путей два:
Напрямую из командной строки контейнера application
docker exec -it application bash \ php public/index.php
Из командной строки хоста через контейнер application:
docker exec application sh -c "php public/index.php"
Перезапуск php через systemctl - нет, не предусмотрено. Если нужно, то есть довольно подробная инструкция как это сделать.
По поводу подключения по ssh - если речь о том, что сайт размещен на удаленной машине, а подключиться надо с локальной - то да. На локальной машине выполняем:
docker context create remote-env --docker host=ssh://www@example.com docker context use remote-env
В результате у нас локальный docker будет работать с сайтом на удаленном сервере, например команда
docker-exec -it application bash
запустит командную строку bash в контейнере application не локально, а на удаленном сервере.
t38c3j
12.06.2022 18:18+2ports:
- "3306:3306"
а вот так не надо делать, порт будет смотреть в мир, докер вне правил фаервола по умолчанию, надо127.0.0.1:3306:3306
и уже в том же датагрип подключаться через тунель
t38c3j
12.06.2022 18:161.
image: mysql
надо указывать конкретную версию
2.links
устарели
3. certbot можно успешно заменить на traefik
4. у вас окружение для дева но не прода, на прод уже готовые образы доставляются с кодом внутриBlacker
13.06.2022 10:573. certbot можно успешно заменить на traefik
Тот факт, что traefik умеет сам ставить и обновлять сертификаты, это несомненно плюс. Только это не отменяет того, что certbot — просто утилита для работы с сертификатами, а traefik — проксирующий веб-сервер.
Лучше ставить Caddy взамен nginx'а, раз уж на то пошло.
yulchurin
Зачем для одного сайта docker? Пустая трата оперативки
MadridianFox
Например за тем чтобы не париться с тем какие версии по есть в репозиториях того дистрибутива, который сейчас на сервере. Обновить версию докеризированной субд становится в разы проще.
По той же причине не страшно переезжать с какого-нибудь умирающего центоса на убунту.
Запуск прод версии сайта в докере полезно тем, что становится очень просто развернуть dev версию контейнера, т.е. вы получаете окружение максимально похожее на боевое.
Плюсов в этом подходе больше чем минусов. Можно порассуждать о том, что для прода можно выбрать что-то посерьёзнее чем docker-compose, но и кубернетис был бы тут вероятно избыточным.
Так или иначе это уже шаг в сторону современной архитектуры и это лучше чем некоторые альтернативы.
EvilShadow
Вы пробовали? Хотя бы постгрес. Хотя бы между соседними версиями. Допустим, 11 -> 12. Можно даже не слишком большую, гиг на 300. Просто чтобы pg_dump/pg_restore стал слишком долгим для использования в продакшене, где даунтайм имеет значение.
pfffffffffffff
Тогда можно юзать dbaas
Loggus66
О, я скоро буду пробовать. Без простоя, пожалуй, только логическая репликация остаётся, благо с "десятки" она встроенная. И напомню, что этот "вопрос с подковыркой" годами был одним из самых горячих внутри сообщества, в том числе из-за сложностей с обновлением с Postgres ушёл Uber, здесь обсуждение их проблем и упоминается много всего интересного, включая некое таинственное "коммерческое решение", которое позволяет снизить простой.
EvilShadow
Нет, совсем без даунтайма не обязательно. 5-10 минут может быть допустимо, часы - вряд ли. Речь о том, что обкатанные, проверенные временем быстрые способы типа `pg_upgrade --link` легко и просто работают на машинах. Но в контейнерах (а ещё лучше в кубе) это превращается в упражнения, без которых лучше бы обойтись. При этом проблема даже не в контейнерах как таковых: если данные хранятся локально, то контейнеры бесплатны с т.з. производительности (но не когнитивной нагрузки). Проблема в докере и его PID 1. С тем же LXC сложностей нет.
Поэтому я бы десять раз подумал, прежде чем применять докер для stateful нагрузок вообще и баз в частности.
MadridianFox
Если говорить о системе где недопустим даунтайм, то да, схема развёртывания тут неверна.
Но когда видишь в одном месте битрикс и запуск бд и веб-сервера на одной машине, остаётся только порадоваться что тут используется докер и что все компоненты не завернуты в один контейнер.
undersunn Автор
Вы буквально описали мою ситуацию. Имеется сервер под Centos 6, на котором на php 7.1 работает сайт. И надо как-то все это обновить до актуальных версий, причем в процессе обновления надо временно приостановиться на PHP 7.4, чтобы успешно установить самые старые обновления CMS, которые на версии PHP младше 7.4 не установятся
raamid
Я например, очень активно использую Docker в разработке. Недавно как раз настраивал себе среду для веб-приложений с HTTPS и прочими свистоперделками вроде автоматической сборкой проекта и перезагрузкой сервера по сохранению файла снаружи контейнера.
dolfinus
Если речь не идёт о Docker Desktop, а о нативных контейнерах (Linux), то оверхеда нет ни по памяти, ни по CPU, потому что это всего лишь механизм изоляции процессов на уровне ядра, а не виртуалка или эмуляция. А вот по сети оверхед есть
Tanner
Ядро-то изолировано, а над ядром – отдельный юзерспейс, ортогональный хостовому. Как тут может не быть оверхеда по памяти?
MadridianFox
Легко. Механизмы изоляции процессов, на которых основана контейнеризация, используются даже тогда, когда вы не используете контейнеры. Например systemd использует и cgroups и namespaces. Просто на всякий случай, вдруг вы захотите ими управлять.
Да, создавая новый нейсмпейс вы выделяете в ядре какие-то дополнительные дескрипторы. Это можно посчитать тратой памяти. Потратите несколько килобайт. Думаю это где-то в пределах погрешности, т.к. процессы внутри контейнеров потребляют на два-три порядка больше.
Tanner
Выражусь поконкретней. У меня хост-система, допустим, Debian с glibc, а в контейнере – Alpine с musl. Соответственно, хостовые приложения у нас связаны с одной библиотекой, а контейнеризованные – с другой. Так под glibc и musl что, не выделяется память отдельно под ту и другую?
Далее, представим себе, что у нас уже контейнеризовано что-то на Alpine версии 3.15 с musl=1.2.2, и тут мы создаём новый контейнер с Alpine 3.16 и musl=1.2.3. У нас опять происходит магия и никакого оверхеда, или всё-таки расходуется память под обе версии musl?
А если ещё чуть-чуть подумать, разве не весь юзерспейс у нас ведёт себя точно так же? Библиотеки, утилиты, шеллы? На хосте и в каждом контейнере?
dolfinus
Ну так и на хосте, без контейнеров, можно запустить разные версии приложений с разными версиями либ под капотом. Но почему-то никто не называет это оверхедом, это просто следствие использования разных версий приложений одновременно. Если файлы разные, то кэшироваться они будут независимо друг от друга.
А вот если запускать несколько контейнеров из одного образа, где версии приложений/либ совпадают, то по сути все контейнеры ссылаются на один и тот же файл внутри образа, поэтому он загружается в память только один раз. Дисковый кэш находится уровнем ниже разделения на namespace, поэтому для него не важно, запустили приложение на хосте, в контейнере или в нескольких.
Более подробно можно почитать здесь: https://biriukov.dev/docs/page-cache/7-how-much-memory-my-program-uses-or-the-tale-of-working-set-size/
И да, не userspace, а namespace. Причем это не один namespace на контейнер, а несколько - изолируется файловая система, процессы, сеть и т.п.:
https://habr.com/ru/company/ruvds/blog/592057/
https://habr.com/ru/company/ruvds/blog/593335/
Tanner
Ключевое слово «можно». При том, что обычно так не делается. Обычно все приложения в дистрибутиве линкуются с одной версией либы, насколько это возможно. На то он и дистрибутив. Докеризация, напротив, позволяет для каждого приложения тащить разные версии всего подряд – не только слинкованных либ, а вообще всего, кроме ядра – при том, что никакой необходимости в этом нет.
Я это прекрасно понимаю, но мне почему-то кажется, что на практике такие счастливые совпадения происходят нечасто.
А давайте я вам тоже ссылку дам: https://en.wikipedia.org/wiki/User_space_and_kernel_space.
Suvitruf
У меня так лендинг и документация в альпинку завёрнуты. При пуше в мастер Github Actions собирает докер контейнер, пушит в хаб, а на беке watchtower получает обновление и разворачивает образ. Довольно удобно.