Предисловие
Так сложилось, что мне приходится работать над большим количеством сайтов, задачи решать так же разные - от настроек сервера до "сверстать форму". И вот на одном из проектов возникла задача - обновиться до актуальной версии 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)
 - php712.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 не локально, а на удаленном сервере.  - t38c3j12.06.2022 18:18+2- ports:
 - "3306:3306"
 а вот так не надо делать, порт будет смотреть в мир, докер вне правил фаервола по умолчанию, надо- 127.0.0.1:3306:3306и уже в том же датагрип подключаться через тунель
 
 
 - t38c3j12.06.2022 18:16- 1. - image: mysqlнадо указывать конкретную версию
 2.- linksустарели
 3. certbot можно успешно заменить на traefik
 4. у вас окружение для дева но не прода, на прод уже готовые образы доставляются с кодом внутри - Blacker13.06.2022 10:57- 3. 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 получает обновление и разворачивает образ. Довольно удобно.