Предисловие

Так сложилось, что мне приходится работать над большим количеством сайтов, задачи решать так же разные - от настроек сервера до "сверстать форму". И вот на одном из проектов возникла задача - обновиться до актуальной версии php (8.1 на момент написания), обновить до актуальной версии CMS (1C Bitrix), ну и в целом, "довести до ума".
Поскольку проект оброс значительным количеством функционала, не связанного с сайтом напрямую (инкрементальные и полные бэкапы по расписанию с выгрузкой в облако, составление словарей, синхронизации с разными поставщиками), а работы ведутся в 3 окружениях (локально, тестовая площадка и продакшн сайт), то я решил, что это будет хорошей возможностью перенести всю инфраструктуру на контейнеры Docker.
Поскольку технология уже устоявшаяся, то ожидалось, что найдется готовый шаблон сервера "из коробки", который подойдет под наши нужды. Но поискав, не удалось найти полноценного решения - везде были какие-то нюансы, из-за которых решение не подходило. В результате был собран собственный сервер для сайта на 1С Битрикс. После чего из сервера было вырезано все, что связано с этой CMS и теперь он может использоваться под другие проекты без ограничений.

Код доступен на github.

Компоненты сервера

Для полноценной работы сервера нам нужны следующие компоненты:

  • база данных (MySQL);

  • PHP;

  • NGINX;

  • прокси для отправки почты (msmtp);

  • composer;

  • letsencrypt сертификаты;

  • резервное копирование и восстановление;

  • опционально - облако для хранения бэкапов.

Так же нам нужно по расписанию запускать разные действия. Для этого будет использоваться crontab на хосте, а не в контейнерах.

Перед началом работ

На сервере нам понадобится 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)


  1. yulchurin
    11.06.2022 20:12
    -1

    Зачем для одного сайта docker? Пустая трата оперативки


    1. MadridianFox
      11.06.2022 21:59
      +8

      Например за тем чтобы не париться с тем какие версии по есть в репозиториях того дистрибутива, который сейчас на сервере. Обновить версию докеризированной субд становится в разы проще.

      По той же причине не страшно переезжать с какого-нибудь умирающего центоса на убунту.

      Запуск прод версии сайта в докере полезно тем, что становится очень просто развернуть dev версию контейнера, т.е. вы получаете окружение максимально похожее на боевое.

      Плюсов в этом подходе больше чем минусов. Можно порассуждать о том, что для прода можно выбрать что-то посерьёзнее чем docker-compose, но и кубернетис был бы тут вероятно избыточным.

      Так или иначе это уже шаг в сторону современной архитектуры и это лучше чем некоторые альтернативы.


      1. EvilShadow
        11.06.2022 22:06
        +3

        Обновить версию докеризированной субд становится в разы проще.

        Вы пробовали? Хотя бы постгрес. Хотя бы между соседними версиями. Допустим, 11 -> 12. Можно даже не слишком большую, гиг на 300. Просто чтобы pg_dump/pg_restore стал слишком долгим для использования в продакшене, где даунтайм имеет значение.


        1. pfffffffffffff
          11.06.2022 22:36
          -1

          Тогда можно юзать dbaas


        1. Loggus66
          11.06.2022 23:18

          О, я скоро буду пробовать. Без простоя, пожалуй, только логическая репликация остаётся, благо с "десятки" она встроенная. И напомню, что этот "вопрос с подковыркой" годами был одним из самых горячих внутри сообщества, в том числе из-за сложностей с обновлением с Postgres ушёл Uber, здесь обсуждение их проблем и упоминается много всего интересного, включая некое таинственное "коммерческое решение", которое позволяет снизить простой.


          1. EvilShadow
            12.06.2022 20:07
            +2

            Нет, совсем без даунтайма не обязательно. 5-10 минут может быть допустимо, часы - вряд ли. Речь о том, что обкатанные, проверенные временем быстрые способы типа `pg_upgrade --link` легко и просто работают на машинах. Но в контейнерах (а ещё лучше в кубе) это превращается в упражнения, без которых лучше бы обойтись. При этом проблема даже не в контейнерах как таковых: если данные хранятся локально, то контейнеры бесплатны с т.з. производительности (но не когнитивной нагрузки). Проблема в докере и его PID 1. С тем же LXC сложностей нет.

            Поэтому я бы десять раз подумал, прежде чем применять докер для stateful нагрузок вообще и баз в частности.


        1. MadridianFox
          12.06.2022 09:40
          +4

          Если говорить о системе где недопустим даунтайм, то да, схема развёртывания тут неверна.

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


      1. undersunn Автор
        12.06.2022 12:47

        Вы буквально описали мою ситуацию. Имеется сервер под Centos 6, на котором на php 7.1 работает сайт. И надо как-то все это обновить до актуальных версий, причем в процессе обновления надо временно приостановиться на PHP 7.4, чтобы успешно установить самые старые обновления CMS, которые на версии PHP младше 7.4 не установятся


    1. raamid
      11.06.2022 23:40

      Зачем для одного сайта docker? Пустая трата оперативки

      Я например, очень активно использую Docker в разработке. Недавно как раз настраивал себе среду для веб-приложений с HTTPS и прочими свистоперделками вроде автоматической сборкой проекта и перезагрузкой сервера по сохранению файла снаружи контейнера.


    1. dolfinus
      12.06.2022 01:04
      +1

      Если речь не идёт о Docker Desktop, а о нативных контейнерах (Linux), то оверхеда нет ни по памяти, ни по CPU, потому что это всего лишь механизм изоляции процессов на уровне ядра, а не виртуалка или эмуляция. А вот по сети оверхед есть


      1. Tanner
        12.06.2022 03:21
        +1

        Ядро-то изолировано, а над ядром – отдельный юзерспейс, ортогональный хостовому. Как тут может не быть оверхеда по памяти?


        1. MadridianFox
          12.06.2022 09:29
          +1

          Легко. Механизмы изоляции процессов, на которых основана контейнеризация, используются даже тогда, когда вы не используете контейнеры. Например systemd использует и cgroups и namespaces. Просто на всякий случай, вдруг вы захотите ими управлять.

          Да, создавая новый нейсмпейс вы выделяете в ядре какие-то дополнительные дескрипторы. Это можно посчитать тратой памяти. Потратите несколько килобайт. Думаю это где-то в пределах погрешности, т.к. процессы внутри контейнеров потребляют на два-три порядка больше.


          1. Tanner
            12.06.2022 10:00

            Выражусь поконкретней. У меня хост-система, допустим, Debian с glibc, а в контейнере – Alpine с musl. Соответственно, хостовые приложения у нас связаны с одной библиотекой, а контейнеризованные – с другой. Так под glibc и musl что, не выделяется память отдельно под ту и другую?

            Далее, представим себе, что у нас уже контейнеризовано что-то на Alpine версии 3.15 с musl=1.2.2, и тут мы создаём новый контейнер с Alpine 3.16 и musl=1.2.3. У нас опять происходит магия и никакого оверхеда, или всё-таки расходуется память под обе версии musl?

            А если ещё чуть-чуть подумать, разве не весь юзерспейс у нас ведёт себя точно так же? Библиотеки, утилиты, шеллы? На хосте и в каждом контейнере?


            1. dolfinus
              12.06.2022 10:42
              +2

              Ну так и на хосте, без контейнеров, можно запустить разные версии приложений с разными версиями либ под капотом. Но почему-то никто не называет это оверхедом, это просто следствие использования разных версий приложений одновременно. Если файлы разные, то кэшироваться они будут независимо друг от друга.

              А вот если запускать несколько контейнеров из одного образа, где версии приложений/либ совпадают, то по сути все контейнеры ссылаются на один и тот же файл внутри образа, поэтому он загружается в память только один раз. Дисковый кэш находится уровнем ниже разделения на 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/


              1. Tanner
                12.06.2022 10:59

                Ну так и на хосте, без контейнеров, можно запустить разные версии приложений с разными версиями либ под капотом.

                Ключевое слово «можно». При том, что обычно так не делается. Обычно все приложения в дистрибутиве линкуются с одной версией либы, насколько это возможно. На то он и дистрибутив. Докеризация, напротив, позволяет для каждого приложения тащить разные версии всего подряд – не только слинкованных либ, а вообще всего, кроме ядра – при том, что никакой необходимости в этом нет.

                А вот если запускать несколько контейнеров из одного образа, где версии приложений/либ совпадают, то по сути все контейнеры ссылаются на один и тот же файл внутри образа

                Я это прекрасно понимаю, но мне почему-то кажется, что на практике такие счастливые совпадения происходят нечасто.

                И да, не userspace, а namespace.

                А давайте я вам тоже ссылку дам: https://en.wikipedia.org/wiki/User_space_and_kernel_space.


    1. Suvitruf
      12.06.2022 01:51

      У меня так лендинг и документация в альпинку завёрнуты. При пуше в мастер Github Actions собирает докер контейнер, пушит в хаб, а на беке watchtower получает обновление и разворачивает образ. Довольно удобно.


  1. php7
    12.06.2022 11:14
    -3

    К mysql можно достучаться из мира? По какому адресу?

    Можно ли выполнить из командной строки php-скрипт? Как?

    Можно ли перезапускать php через systemctl?

    Можно ли подключиться к контейнеру по ssh?


    1. undersunn Автор
      12.06.2022 12:34
      +4

      К mysql достучаться - нельзя, поскольку на хосте открыты только 22, 80, 443 порты. Если есть такая необходимость - то надо на хосте открыть порт 3306, а в compose-app.yml в сервисе database в секции "ports" указать:

      ports:
        - "3306:3306"

      Выполнить скрипт из командной строки - можно. Поскольку PHP у нас только в контейнере application, то запускать php скрипт мы будем именно через этот контейнер. Путей два:

      1. Напрямую из командной строки контейнера application

      docker exec -it application bash \
      php public/index.php
      1. Из командной строки хоста через контейнер 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 не локально, а на удаленном сервере.


      1. t38c3j
        12.06.2022 18:18
        +2

        ports:
        - "3306:3306"

        а вот так не надо делать, порт будет смотреть в мир, докер вне правил фаервола по умолчанию, надо 127.0.0.1:3306:3306 и уже в том же датагрип подключаться через тунель


  1. t38c3j
    12.06.2022 18:16

    1. image: mysql надо указывать конкретную версию
    2. links устарели
    3. certbot можно успешно заменить на traefik
    4. у вас окружение для дева но не прода, на прод уже готовые образы доставляются с кодом внутри


    1. Blacker
      13.06.2022 10:57

      3. certbot можно успешно заменить на traefik

      Тот факт, что traefik умеет сам ставить и обновлять сертификаты, это несомненно плюс. Только это не отменяет того, что certbot — просто утилита для работы с сертификатами, а traefik — проксирующий веб-сервер.

      Лучше ставить Caddy взамен nginx'а, раз уж на то пошло.