Локальный веб-сервер для разработки с помощью Docker
Старший разработчик ГК Юзтех
К вам в отдел выходит новый коллега-разработчик и, прежде чем брать первые задачи в одном из проектов, первым делом ему нужно запустить его у себя локально.
Если это 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 файла ниже, о синтаксисе можете прочитать в документации, а я поясню, что именно будет выполняться.
На основе указанных образов запустяться контейнеры
В контейнеры пробросятся созданные нами ранее тома
В контейнерах откроются порты
Создается внутренняя сеть, которая упростит взаимодействие контейнеров друг с другом.
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.