Семен Тюреньков

Старший разработчик ГК Юзтех


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

Если это Senior Full Stack разработчик с опытом администрирования Linux, то установка и настройка конфигов Nginx, PHP-fpm, MariaDB для него не будут проблемой (а может и с Docker даже знаком?). 

Разработчик Middle уровня (особенно без опыта с backend) возможно пользуется одним из готовых решений под Windows/MacOS.

Junior верстальщик, в свою очередь, раньше не запускал приложение работающее на PHP на своем компьютере вообще, и вот-вот попробует в первый раз.

Было такое? У меня было. Случалось даже поздним вечером помогать новичку с установкой или решением проблемы, возникшей в ходе установки.

А потом, еще через некоторое время, из-за разных конфигов или окружения возникали и новые проблемы из разряда “на моем компьютере же все работает”, которые в том числе могут появиться из-за разных настроек готовых сборок.

Я начал регулярно сталкиваться с этими проблемами, когда стал ответственным за релизы и работу команды. Это было сравнительно недавно.Docker к этому времени уже был широко известен и даже использовался другими отделами в компании, в том числе отделом DevOps на production, но для разработки в образе с прода не хватало компонентов. 

Чтобы решить проблему с локальным окружением, которое у всех в команде должно быть максимально похожим на Production, и удобным  для разработки, я начал сам погружаться в документацию и примеры по Docker. В этой статье я поделюсь с вами инструкцией как сделать аналогичный образ для нужд своей команды.

Установка

Хотя в результате вы получите практически одинаковые локальные окружения, установка на Windows, Linux и Mac несколько отличается.

Я расскажу о двух способах установки - для Windows и Mac. 

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

Docker Desktop

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

Немного теории в контексте веб-разработки своими словами (для более официального описания посмотрите документацию):

  • Образ - это конфигурация системы с программами-компонентами и их зависимостями. Образы могут быть как отдельные для каждого компонента (Nginx, PHP-fpm, MariaDB, Redis, Postfix и т.п.), так можно создать и единый образ сразу со всеми компонентами и самим веб-сайтом внутри.

  • Том - это постоянное хранилище данных, используемое контейнерами. Зачем это нужно? Все данные появляющиеся в контейнере удаляются при перезапуске, кроме тех, что заложены в образ. Чтобы сохранить какие-то данные, папки их содержащие нужно подключить как тома.

  • Контейнер - это изолированная система, созданная из образа и, возможно, с подключенными к ней томами, в котором выполняются нужные вам программы.

Дополнительная информация для Windows: для выполнения шагов в этой статье, ваш компьютер должен поддерживать WSL2. 

При установке Docker Desktop спросит хотите ли вы использовать WSL2 и вам следует согласиться. К тому же это рекомендуется и самим Docker.

Если вы пропустили этот шаг и Docker у вас давно, попробуйте включите поддержку WSL2 через настройки.

Хранение данных и проекта

Далее вам нужно подготовить, где вы будете хранить данные - папки с проектами, базы данных, nginx конфиги сайтов и SSL сертификаты.

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

Лично мне сложно работать с проектом, когда страницы грузятся долго. Лучшим решением будет хранить файлы в Linux подсистеме, поэтому для пользователей Windows рекомендую поставить еще одну подсистему через WSL2, например Debian или Ubuntu и хранить проекты там. 

Создайте или определите 4 папки в проекте, которые вы будете использовать в .env при установке Docker окружения:

Я рекомендую все хранить внутри той же директории, что и проект:

SITE_DOMAIN=”localhost”
SITE_PATH=”$PWD” 
SSL_PATH=”$PWD/ssl”
DB_PATH=”$PWD/db”
NGINX_CONF_PATH=”$PWD/docker/nginx/site-conf”

Установочный скрипт

Создайте файл install.sh, и все, с кем вы поделитесь проектом, смогут запустить его, чтобы автоматически скачать базовые образы, сбилдить проектные образы и создать тома из папок указанных в .env

#!/bin/bash

# скрипт прочитает .env файл и возьмет переменные

if [ -f .env ]; then
    export $(cat .env | xargs)
fi

# небольшая функция которая будет проверять существует ли том, и если нет, то создаст 
# его из указанных в .env путей. Если захотите переопределить тома, их надо будет  
# удалить в интерфейсе Docker Desktop или через командную строку

create_volume() {
    local volume_name=$1
    local volume_path=$2

            echo "Creating volume $volume_name..." \
        && docker volume create --driver local --opt type=none --opt device=$volume_path --opt o=bind $volume_name
}

# и, наконец, основное тело скрипта, создает папки из указанных путей, на случай если они # еще не существуют. Затем входит в подпапки nginx и php-fpm для сборки образа (об 
# этом расскажу еще чуть позже). И создает тома используя указанную выше функцию.

mkdir -p $SSL_PATH \
&& mkdir -p $SITE_PATH \
&& mkdir -p $DB_PATH \
&& mkdir -p $NGINX_CONF_PATH \
&& openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout $SSL_PATH/nginx.key -out $SSL_PATH/nginx.crt \
&& sed -i ‘’ "s|server_name localhost;|server_name ${SITE_DOMAIN};|g" "${NGINX_CONF_PATH}/website.conf" \
&& cd docker/nginx \
&& sh builddocker.sh \
&& cd ../php-fpm \
&& sh builddocker.sh \
&& create_volume ssl $SSL_PATH \
&& create_volume site $SITE_PATH \
&& create_volume db $DB_PATH \
&& create_volume nginx-conf $NGINX_CONF_PATH

Docker Compose

Создайте файл docker-compose.yml, а также добавьте в .env еще одну переменную - 

MYSQL_ROOT_PASSWORD=password

Содержимое docker-compose файла ниже, о синтаксисе можете прочитать в документации, а я поясню, что именно будет выполняться.

  1. На основе указанных образов запустяться контейнеры

  2. В контейнеры пробросятся созданные нами ранее тома

  3. В контейнерах откроются порты

  4. Создается внутренняя сеть, которая упростит взаимодействие контейнеров друг с другом.

version: '3.1'

services:
  db:
    image: mariadb:11.2.2
    container_name: db
    restart: always
    volumes:
      - db:/var/lib/mysql
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    networks:
      - localhost_net

  php-fpm-custom:
    image: php-fpm-custom:latest
    container_name: php-fpm-custom
    restart: always
    volumes:
      - www:/var/www
    ports:
      - 9000:9000
    networks:
      - localhost_net

  redis:
    image: redis:7.2.4
    container_name: redis
    restart: always
    ports:
      - 6379:6379
    networks:
      - localhost_net  
    expose:
      - '6379'

  nginx-custom:
    depends_on:
      - db
      - php-fpm-custom
    image: nginx-custom:latest
    container_name: nginx-custom
    restart: always
    volumes:
      - ssl:/etc/nginx/ssl
      - www:/var/www
      - nginx-conf:/etc/nginx/sites-enabled
    ports:
      - 80:80
      - 443:443
    networks:
      localhost_net:
        aliases:
          - ${SITE_DOMAIN}

node:
    image: node:21.5
    container_name: node
    restart: always
    networks:
      - localhost_net
    command: "tail -f /dev/null"

  phpmyadmin:
    depends_on:
      - db
    image: phpmyadmin:5.2.1
    container_name: phpmyadmin
    restart: always
    ports:
      - 8081:80
    environment:
      - PMA_ARBITRARY=1
    networks:
      - localhost_net

networks:
  localhost_net:

volumes:
  ssl:
    external: true
  nginx-conf:
    external: true
  www:
    external: true
  db:
    external: true

В результате у нас будут контейнеры с Nginx, PHP-fpm, MariaDB, Redis, Phpmyadmin и NodeJS

Небазовые образы

Если по каким-то причинам базового образа недостаточно, вы можете создать папки в проекте с Dockerfile для вашего индивидуального образа. В моем случае мне потребовалось изменить некоторые настройки в Nginx и PHP-fpm образах, поэтому создайте папки nginx и php-fpm соответственно.

В каждой папке создайте Dockerfile и builddocker.sh скрипт.

Nginx Dockerfile

# Используем этот базовый образ
FROM nginx:1.25.3

# Установим дополнительные пакеты, которые оказались нужны в этом контейнере
RUN apt-get update \
    && apt-get -y install lsb-release libssl-dev ca-certificates curl openssl 

# Если хотите пробросить кастомный основной конфиг nginx, то это можно сделать здесь
COPY nginx.conf /etc/nginx/nginx.conf

# Откроем порты
EXPOSE 80 443

# Запустим Nginx как foreground процесс, чтобы контейнер не выключался
CMD ["nginx", "-g", "daemon off;"]

Nginx builddocker.sh

Этот скрипт будет создавать образ с тегом :latest в вашей локальной системе, а также удалять старый образ, если вдруг вы обновите конфигурацию, который потом будет использовать docker compose.

#!/bin/bash

docker build -t nginx-custom --label nginx-custom . \
&& docker image prune --force --filter='label=nginx-custom'

Основной конфиг nginx.conf

В моем случае я только увеличил client_max_body_size, возможно еще gzip и tcp директивы, не помню точно стандартный конфиг, но настройка конфига nginx под себя - это тема отдельных статей. 

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    tcp_nodelay on;
    tcp_nopush on;

    keepalive_timeout  35;

    gzip  on;
    client_max_body_size 20M;
    include /etc/nginx/conf.d/*.conf;
}

PHP-fpm Dockerfile

# В моем случае я использую 8.1 т.к. Wordpress ещё слабо поддерживает версии выше, но вы можете указать нужную вам версию PHP.
FROM php:8.1-fpm

# добавим в PATH композер, который мы установим далее.
ENV PATH="/root/.composer/vendor/bin:${PATH}"

# А вот здесь устанавливаем довольно много всего - рекомендуемые php-extensions для Wordpress, Composer, WP-CLI, 
RUN apt-get update \
    && apt-get install -y libzip-dev unzip libpq-dev libmagickwand-dev libpng-dev libjpeg-dev libfreetype6-dev less \
    && rm -rf /var/lib/apt/lists/* \
    && docker-php-ext-install zip pdo_pgsql pgsql bcmath opcache \
    && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
    && chmod +x /usr/local/bin/composer \
    && curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
    && chmod +x wp-cli.phar \
    && mv wp-cli.phar /usr/local/bin/wp \
	&& pecl install imagick && docker-php-ext-enable imagick \
	&& docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) gd

# Если хотите пробросить кастомный php.ini, то это можно раскомментировать
# COPY php.ini /usr/local/etc/php/php.ini

# Expose port 9000 for PHP-FPM
EXPOSE 9000

# Start PHP-FPM
CMD ["php-fpm"]

Кастомизированный php.ini

Не буду публиковать весь огромный файл конфига, но стандартно я в нем меняю следующие настройки:

upload_max_filesize = 16M

post_max_size = 16M

Следует помнить, что за основу нужно брать стандартный конфиг от выбранной вами версии PHP, найти его можно например запустив контейнер сначала без пробрасывания собственного php.ini.

Nginx конфиги для проектов

В указанной в .env папке для nginx конфигов сайтов создайте файл website.conf

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

В моем случае, конфиг для сайта на Wordpress вот такой:

server {
    listen 80;
    server_name localhost;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name localhost;

    gzip on;
    gzip_comp_level 5;
    gzip_min_length 256;
    gzip_proxied any;
    gzip_vary on;

    ssl_certificate /etc/nginx/ssl/nginx.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx.key;

    root /var/www/site;
    index index.php;

    location / {
            proxy_request_buffering off;
            try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass php-fpm-custom:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_index index.php;

        fastcgi_buffer_size 128k;
        fastcgi_buffers 4 256k;
        fastcgi_busy_buffers_size 256k;
    }
}

Важно! Выбранные вами домены надо также указать в hosts родительской системы или прописать в DNS, если разворачиваете на сервере!

Например:

127.0.0.1 mysite.test

Запуск окружения

Если вы еще этого не сделали, то выполните

sh install.sh в вашем терминале.

Затем вы можете запустить окружение командой
docker-compose up -d

И остановить командой
docker-compose down

Алиасы

Так как все привычные вам инструменты разработки тоже оказались в контейнере, то это скажется на процессе разработки.

Привычные вам короткие команды потребуется запускать через docker exec или docker run, поэтому предлагаю использовать алиасы. Да и линтеры не будут выполняться если в родительской системе их нет и не созданы алиасы на контейнер.

Добавьте следующее содержимое в файл ~/.bashrc (если используете bash в качестве терминала):

alias composer='docker exec -it -w /var/www/site php-fpm-custom composer'
alias phpcs='docker run --rm -it -v $(pwd):/app -w /app php-fpm-custom vendor/bin/phpcs'
alias phpcbf='docker run --rm -it -v $(pwd):/app -w /app php-fpm-custom vendor/bin/phpcbf'
alias wp='docker exec -it -w /var/www/site php-fpm-custom wp'
alias node='docker run --rm -it -v $(pwd):/app -w /app node:21.5 node'
alias npm='docker run --rm -it -v $(pwd):/app -w /app node:21.5 npm'
alias npx='docker run --rm -it -v $(pwd):/app -w /app node:21.5 npx'
alias yarn='docker run --rm -it -v $(pwd):/app -w /app node:21.5 yarn'

Алиас позволит вам привычно писать команду, но выполняться будет другая, указанная вами. То есть написав npm run build, вы фактически выполните команду docker run --rm -it -v $(pwd):/app -w /app node:21.5 npm run build

Если у вас в родительской системе уже стоят свои версии ПО и вы хотите оставить их доступными, вы можете изменить алиасы, добавив суффиксы в команды: -docker

Таким образом, вы сможете выполнять npm или npm-docker в зависимости от того, хотите ли вы использовать родительские программы или те, что находятся в контейнере.

IDE

Я использую VScode, поэтому могу рассказать только про эту IDE.

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

В моем случае это коснулось линтеров PHP и JS. 

В рекомендациях своего проекта я указал следующие:

"bradlc.vscode-tailwindcss",

"dbaeumer.vscode-eslint",

"bmewburn.vscode-intelephense-client",

"mtbdata.vscode-phpsab-docker"

Также при использовании подсистемы для хранения файлов необходимо будет установить WSL extension и запускать VScode в режиме WSL.

Заключение

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

Возможно, помимо использования разработчиками, вы также сможете внедрить процессы тестирования веток QA командой на их локальных машинах. 

Также эти образы можно использовать в пайплайнах GitLab для автоматических линтов и тестов MR.

Комментарии (5)


  1. Tony-Sol
    16.04.2024 11:47
    +2

    Звонили из 2015, просили вернуть актуальную статью (простите, шутка)

    Мне сейчас тяжело представить веб-разработчика уровня junior, который хотя бы не слышал про docker, не говоря уже о middle и senior

    В целом, ок, как гайд «для самых маленьких», вот только такого рода гайдов кажется и так довольно много


  1. Newcss
    16.04.2024 11:47
    +3

    Большинство Мидлов с различных месячных курсов о докере только слышали, к сожалению...


    1. Tony-Sol
      16.04.2024 11:47

      ну так с месячных курсов, де-факто, максимум стажерами выходят


  1. Rufus57
    16.04.2024 11:47

    Как один из вариантов, вместо Docker Desktop можно использовать Portainer. Вместо nginx, traefik.


    1. Tony-Sol
      16.04.2024 11:47

      Portainer это же только web gui над docker, а из реальных альтернатив, можно рассмотреть podman (просто) или lima (посложнее), но зачем)

      А на место nginx можно рассмотреть angie, но зачем)