Docker для локальной веб-разработки - часть 4
Всем привет! Эта статья продолжает серию переводов замечательных статей об использовании Docker для локальной веб-разработки.
В этой серии⚓︎
- Введение: почему это должно вас волновать?
- Часть 1: базовый стек LEMP
- Часть 2: посадите свои образы на диету
- Часть 3: трёхуровневая архитектура с фреймворками
- Часть 4: сглаживание ситуации с помощью Bash ⬅️ вы здесь
- Часть 5: HTTPS для всего
- Часть 6: открываем локальный контейнер для доступа в Интернет
- Часть 7: использование многоэтапной сборки для внедрения воркера
- Часть 8: запланированные задачи
- Заключение: куда идти дальше
Введение⚓︎
По мере формирования нашей среды разработки количество команд, которые нам необходимо запомнить, начинает расти. Вот несколько из них в качестве напоминания:
docker compose up -dдля запуска контейнеров;docker compose logs -f nginxдля просмотра логов Nginx;docker compose exec backend php artisanдля запуска команд Artisan (или сrun --rm, если контейнер не запущен);docker compose exec frontend yarnдля запуска команд Yarn (то же самое, что и выше);- и т.д.
Очевидно, что ни один из приведенных выше примеров невозможно запомнить, и при некоторой практике любой человек со временем будет знать их наизусть. Тем не менее, это очень много текста для многократного набора, и если вы давно не использовали определенную команду, поиск правильного синтаксиса может занять значительное количество времени.
Кроме того, рамки этой серии уроков довольно ограничены; на практике вы, скорее всего, будете иметь дело с проектами гораздо более сложными, чем этот, требующими гораздо большего количества команд.
Нет смысла внедрять среду, которая в итоге увеличивает умственную нагрузку разработчика. К счастью, существует отличный инструмент, который может помочь нам смягчить эту проблему, о котором вы наверняка хотя бы слышали и который присутствует практически везде: Bash. Без особых усилий Bash позволит нам добавить слой поверх Docker, чтобы абстрагироваться от большинства сложностей, и представить вместо этого стандартизированный, удобный интерфейс.
Предполагаемая отправная точка этого руководства — место, где мы остановились в конце предыдущей части, соответствующее ветке part-3 репозитория.
Если вы предпочитаете, вы также можете напрямую перейти к ветке part-4, которая является конечным результатом сегодняшней статьи.
Bash?⚓︎
Bash существует с 1989 года, что означает, что ему столько же лет, сколько и Интернету, каким мы его знаем. По сути, это командный процессор (оболочка), выполняющий команды, набранные в терминале или считанные из файла (сценарий оболочки).
Bash позволяет пользователям автоматизировать и выполнять огромное количество задач, которые я даже не буду пытаться перечислить. В контексте этой серии статей важно знать, что он может выполнять практически всё, что человек обычно набирает в терминале, что он изначально присутствует на Unix-системах (Linux и macOS, а также Windows через WSL 2).
Его гибкость и переносимость делают его идеальным кандидатом для того, чего мы хотим достичь сегодня. Давайте копнем глубже!
Меню приложения⚓︎
Для начала создадим файл с именем demo в корне нашего проекта (рядом с docker-compose.yml) и дадим ему права на выполнение:
Этот файл будет содержать Bash-скрипт, позволяющий нам взаимодействовать с приложением.
Откройте его и добавьте следующую строку в самом верху:
Это просто указывает, что Bash будет интерпретатором нашего скрипта, и указание, где его найти (/bin/bash — стандартное расположение практически на всех Unix-системах, а также Git Bash в Windows).
Первое, что мы хотим сделать, это создать меню для нашего интерфейса, перечислив доступные команды и способы их использования.
Обновите содержимое файла следующим образом:
#!/bin/bash
case "$1" in
*)
cat << EOF
Command line interface for the Docker-based web development environment demo.
Usage:
demo <command> [options] [arguments]
Available commands:
artisan ................................... Run an Artisan command
build [image] ............................. Build all of the images or the specified one
composer .................................. Run a Composer command
destroy ................................... Remove the entire Docker environment
down [-v] ................................. Stop and destroy all containers
Options:
-v .................... Destroy the volumes as well
init ...................................... Initialise the Docker environment and the application
logs [container] .......................... Display and tail the logs of all containers or the specified one's
restart ................................... Restart the containers
start ..................................... Start the containers
stop ...................................... Stop the containers
update .................................... Update the Docker environment
yarn ...................................... Run a Yarn command
EOF
exit 1
;;
esac
case (иногда известный как switch или match в других языках программирования; дословный перевод — случай) — это базовая управляющая структура, позволяющая нам выполнять различные действия в зависимости от значения $1, причем $1 является первым параметром, передаваемым в скрипт demo.
Например, при выполнении следующей команды $1 будет содержать строку unicorn:
Пока мы рассмотрим только случай по умолчанию, который обозначается *. Другими словами, если мы вызовем наш скрипт без какого-либо параметра или с параметром, значение которого не является частным случаем переключателя, на экран будет выведено меню.
Теперь нам нужно сделать этот скрипт доступным из любого места терминала. Для этого добавьте следующую функцию в локальный файл .bashrc (или .zshrc, или любой другой в соответствии с вашей конфигурацией):
Примечание
Подождите. Что?
Каждый раз, когда вы открываете новое окно терминала, Bash будет пытаться прочитать содержимое некоторых файлов, если он сможет их найти. Эти файлы содержат команды и инструкции, которые вы хотите, чтобы Bash выполнял при запуске, например, обновить переменную $PATH, запустить где-нибудь сценарий или, в нашем случае, сделать функцию доступной глобально. Можно использовать разные файлы, но для простоты мы ограничимся обновлением или созданием файла .bashrc в вашей домашней папке и добавим в него функцию demo, описанную выше:
С этого момента каждый раз, когда вы открываете окно терминала, этот файл будет прочитан, а функция demo станет доступна глобально. Это будет работать в любой операционной системе (включая Windows, при условии, что вы делаете это из Git Bash или из выбранного вами терминала).
Обязательно замените /PATH/TO/YOUR/PROJECT на абсолютный путь к корню проекта (если вы не знаете, что это такое, запустите pwd из папки, в которой находится файл docker-compose.yml, и скопируйте и вставьте результат). Функция, по сути, меняет текущий каталог на корень проекта (cd /PATH/TO/YOUR/PROJECT) и выполняет скрипт demo с помощью Bash (bash demo), передавая ему все параметры команды ($*), которые в основном представляют собой все символы, встречающиеся после demo.
Например, если вы введёте:
Это то, что функция будет делать за кулисами:
Последняя команда функции (cd -) просто меняет текущий каталог на предыдущий. Другими словами, вы можете запускать demo из любого места — вы всегда будете возвращены в каталог, из которого команда была запущена изначально.
Прежде чем мы продолжим, нам нужно добавить в этот файл ещё кое-что:
Помните, как в предыдущей части мы установили non-root пользователя, чтобы избежать проблем с правами доступа к файлам? Нам нужно было передать ID текущего пользователя хост-машины в файл docker-compose.yml, и мы сделали это с помощью файла .env в корне проекта.
Строка выше позволяет нам экспортировать это значение в переменную окружения, которая напрямую доступна файлу docker-compose.yml.
Это означает, что теперь вы можете удалить эту строку из файла .env (ваше значение для HOST_UID может быть другим):
Этот экспорт будет происходить автоматически каждый раз, когда вы открываете окно терминала, то есть больше нет необходимости вручную устанавливать идентификатор пользователя.
Сохраните изменения и откройте новое окно терминала или исходный файл, чтобы они вступили в силу:
source по сути загрузит содержимое исходного файла в текущий shell, без необходимости его полного перезапуска.
Давайте отобразим наше меню:
Выглядит причудливо, не так ли? Однако пока что ни одна из этих команд ничего не делает. Давайте исправим это!
Примечание
Пользователи Windows: следите за окончанием строки CRLF!
Файлы Unix и файлы Windows используют разные невидимые символы для окончания строки — в первом случае добавляется только символ LF, а во втором — и CR, и LF. Ваш сценарий Bash не будет работать с последним вариантом, поэтому при необходимости измените в файле окончание строки на LF. Если вы не знаете, как поступить, просто найдите в Интернете название вашей IDE, а затем "line endings" — большинство современных текстовых редакторов предлагают простой способ переключения.
В целом, хорошей практикой является создание в корне проекта файла .gitattributes, содержащего строку * text=auto, которая будет автоматически преобразовывать окончания строк при проверке и фиксации (см. здесь).
Основные команды⚓︎
Мы начнем с простой команды, чтобы вы почувствовали вкус. Обновите переключатель в файле demo, чтобы он выглядел следующим образом:
Мы добавили случай start, в котором мы вызываем функцию start без каких-либо параметров. Эта функция ещё не существует — в верхней части файла, под #!/bin/bash, добавьте следующий код:
Эта короткая функция просто запускает ставшую уже привычной команду docker compose up -d, которая запускает контейнеры в фоновом режиме. Обратите внимание, что нам не нужно менять текущий каталог, так как при вызове функции demo мы автоматически попадаем в папку, где находится файл demo (рядом с docker-compose.yml).
Сохраните файл и попробуйте новую команду (неважно, запущены ли уже контейнеры):
Вот и всё! Теперь вы можете запустить свой проект из любого места в терминале с помощью приведенной выше команды, которую гораздо проще набрать и запомнить, чем docker compose up -d.
Давайте попробуем ещё раз, на этот раз для отображения логов. Добавим в структуру ещё один случай:
И соответствующую функцию:
Теперь попробуйте запустить:
Теперь у вас есть команда быстрого доступа к логам контейнеров. Это хорошо, но как насчет отображения логов конкретного контейнера?
Давайте немного изменим структуру:
Вместо прямого вызова функции logs мы теперь передаем ей параметры скрипта, начиная со второго, если таковой имеется (это бит "${@:2}"). Причина в том, что когда мы набираем demo logs nginx, первым параметром скрипта будет logs, а мы хотим передать функции start только nginx.
Обновите функцию logs соответствующим образом:
Используя тот же синтаксис, мы добавляем параметры функции к команде, если таковые имеются, начиная с первого ("${@:1}").
Сохраните файл ещё раз и попробуйте:
Теперь, когда вы поняли принцип, и поскольку большинство других функций работают аналогичным образом, вот остальная часть файла, с некоторыми блочными комментариями, чтобы сделать его более читабельным:
#!/bin/bash
#######################################
# FUNCTIONS
#######################################
# Run an Artisan command
artisan () {
docker compose run --rm backend php artisan "${@:1}"
}
# Build all of the images or the specified one
build () {
docker compose build "${@:1}"
}
# Run a Composer command
composer () {
docker compose run --rm backend composer "${@:1}"
}
# Remove the entire Docker environment
destroy () {
read -p "This will delete containers, volumes and images. Are you sure? [y/N]: " -r
if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit; fi
docker compose down -v --rmi all --remove-orphans
}
# Stop and destroy all containers
down () {
docker compose down "${@:1}"
}
# Display and tail the logs of all containers or the specified one's
logs () {
docker compose logs -f "${@:1}"
}
# Restart the containers
restart () {
stop && start
}
# Start the containers
start () {
docker compose up -d
}
# Stop the containers
stop () {
docker compose stop
}
# Run a Yarn command
yarn () {
docker compose run --rm frontend yarn "${@:1}"
}
#######################################
# MENU
#######################################
case "$1" in
artisan)
artisan "${@:2}"
;;
build)
build "${@:2}"
;;
composer)
composer "${@:2}"
;;
destroy)
destroy
;;
down)
down "${@:2}"
;;
logs)
logs "${@:2}"
;;
restart)
restart
;;
start)
start
;;
stop)
stop
;;
yarn)
yarn "${@:2}"
;;
*)
cat << EOF
Command line interface for the Docker-based web development environment demo.
Usage:
demo <command> [options] [arguments]
Available commands:
artisan ................................... Run an Artisan command
build [image] ............................. Build all of the images or the specified one
composer .................................. Run a Composer command
destroy ................................... Remove the entire Docker environment
down [-v] ................................. Stop and destroy all containers
Options:
-v .................... Destroy the volumes as well
init ...................................... Initialise the Docker environment and the application
logs [container] .......................... Display and tail the logs of all containers or the specified one's
restart ................................... Restart the containers
start ..................................... Start the containers
stop ...................................... Stop the containers
update .................................... Update the Docker environment
yarn ...................................... Run a Yarn command
EOF
exit
;;
esac
Обратите внимание на то, что run --rm используется для выполнения команд Artisan, Composer и Yarn на контейнерах backend и frontend соответственно, что позволяет нам делать это независимо от того, запущены контейнеры или нет.
Также вы заметите, что команда restart — это, по сути, сокращение для demo stop, за которым следует demo start, хотя docker compose restart также является опцией. Причина в том, что последняя команда действительно полезна только для перезапуска процессов контейнеров (например, Nginx, чтобы он подхватил изменения конфигурации сервера). Но, скажем, вы обновили и пересобрали образ: выполнение docker compose restart не приведет к воссозданию соответствующего контейнера на основе новой версии образа, а повторно использует старый контейнер.
По сути, в большинстве случаев остановка и запуск (up) контейнеров с большей вероятностью приведет к желаемому эффекту, чем простой docker compose restart, даже если это займет на несколько секунд больше.
Наконец, поскольку задача функции destroy — удалить все контейнеры, тома и образы, будет довольно неприятно запустить её по ошибке, поэтому я сделал её отказоустойчивой, добавив запрос на подтверждение.
Большинство команд уже рассмотрены, но вы могли заметить, что пара команд всё ещё отсутствует: init и update. Они немного особенные, поэтому им посвящен следующий раздел.
Инициализация и обновление проекта⚓︎
Давайте сделаем шаг назад на минуту. Представьте, что вы получили доступ к репозиторию проекта, чтобы установить его на своей машине. Первое, что вы сделаете, это клонируете его локально и добавите функцию demo в .bashrc, чтобы вы могли взаимодействовать с приложением.
После этого вам нужно будет выполнить следующие действия:
- Скопируйте
.env.exampleв.env; - Сделайте то же самое в
src/backend; - Скачайте и соберите образы;
- Установите зависимости фронтенда;
- Установите зависимости бэкенда;
- Запустите миграцию базы данных бэкенда;
- Сгенерируйте ключ приложения бэкенда;
- Запустите контейнеры.
Хотя слой Bash облегчает прохождение по этому списку, для получения функциональной установки необходимо проделать довольно много работы, и легко пропустить какой-нибудь шаг. Что если вам нужно переустановить среду? Или установить её на машину другого разработчика? Или провести клиента через весь процесс?
К счастью, теперь, когда мы внедрили Bash, автоматизировать вышеперечисленные задачи довольно просто.
Сначала добавьте два недостающих случая в demo:
И соответствующие функции (если ваш файл становится беспорядочным, вы также можете посмотреть на конечный результат здесь):
# Create .env from .env.example
env () {
if [ ! -f .env ]; then
cp .env.example .env
fi
}
# Initialise the Docker environment and the application
init () {
env \
&& down -v \
&& build \
&& docker compose run --rm --entrypoint="//opt/files/init" backend \
&& yarn install \
&& start
}
# Update the Docker environment
update () {
git pull \
&& build \
&& composer install \
&& artisan migrate \
&& yarn install \
&& start
}
Давайте сначала посмотрим на init, задача которой — инициализировать весь проект. Первое, что делает эта функция — вызывает другую функцию, env, которую мы определили прямо над ней, и которая отвечает за создание файла .env как копии .env.example, если он не существует. Следующее, что делает init, это обеспечивает уничтожение контейнеров и томов, поскольку цель состоит в том, чтобы иметь возможность как инициализировать проект с нуля, так и перезагрузить его. Затем он собирает образы (которые также будут загружены при необходимости) и продолжает выполнение какого-либо сценария на контейнере бэкенда. Наконец, он устанавливает зависимости фронтенда и запускает контейнеры.
Вы, вероятно, узнаете большинство пунктов из списка в начале этого раздела, но что это за файл init и entrypoint, о которых мы говорим?
Поскольку бэкенд требует немного больше работы, мы можем изолировать соответствующие шаги в один скрипт, который мы смонтируем на контейнере, чтобы запускать его там. Это означает, что нам нужно сделать небольшое дополнение для сервиса backend в docker-compose.yml:
# Backend Service
backend:
build:
context: ./src/backend
args:
HOST_UID: $HOST_UID
working_dir: /var/www/backend
volumes:
- ./src/backend:/var/www/backend
- ./.docker/backend/init:/opt/files/init
depends_on:
mysql:
condition: service_healthy
Поскольку скрипт будет выполняться на контейнере, нам нужно, чтобы на нем был установлен Bash, которого нет по умолчанию в образе Alpine. Нам нужно соответствующим образом обновить Dockerfile бэкенда (в src/backend):
FROM php:8.1-fpm-alpine
# Install extensions
RUN docker-php-ext-install pdo_mysql bcmath opcache
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
# Configure PHP
COPY .docker/php.ini $PHP_INI_DIR/conf.d/opcache.ini
# Use the default development configuration
RUN mv $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
# Install Bash
RUN apk --no-cache add bash
# Create user based on provided user ID
ARG HOST_UID
RUN adduser --disabled-password --gecos "" --uid $HOST_UID demo
# Switch to that user
USER demo
Соберём образ:
Наконец, давайте создадим файл init в папке .docker/backend:
#!/bin/bash
# Install Composer dependencies
composer install -d "/var/www/backend"
# Deal with the .env file if necessary
if [ ! -f "/var/www/backend/.env" ]; then
# Create .env file
cat > "/var/www/backend/.env" << EOF
APP_NAME=demo
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://backend.demo.test
LOG_CHANNEL=single
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=demo
DB_USERNAME=root
DB_PASSWORD=root
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
EOF
# Generate application key
php "/var/www/backend/artisan" key:generate --ansi
fi
# Database
php "/var/www/backend/artisan" migrate
Давайте немного разберемся с этим. Сначала мы устанавливаем зависимости Composer, указывая папку для запуска composer install с помощью опции -d. Затем мы проверяем, существует ли уже файл .env, и если нет, то создаем его с некоторыми предварительно настроенными параметрами, соответствующими нашей установке Docker. Обратите внимание, что мы оставляем переменную окружения APP_KEY пустой; именно поэтому мы запускаем команду для генерации ключа приложения Laravel сразу после создания файла .env. Затем мы переходим к настройке базы данных.
Как и в случае с файлом demo в начале статьи, нам нужно сделать файл init исполняемым:
Если установить её в качестве точки входа entrypoint для вызываемого контейнера, она будет первым и единственным сценарием, который будет запущен до уничтожения контейнера.
Вы можете опробовать команду сразу же, независимо от текущего состояния вашего проекта:
Однако на данном этапе вы, вероятно, уже позаботились о большинстве шагов, описанных в сценарии (например, о генерации .env-файлов или установке зависимостей). Если вы хотите проверить весь процесс, вы можете запустить demo destroy и либо удалить весь проект, либо начать заново, клонируя свой собственный репозиторий или этот (в последнем случае проверьте ветку part-4), не забыв обновить функцию в .bashrc, если путь изменился. Затем снова запустите demo init.
Лично я считаю, что наблюдение за тем, как весь проект сам себя настраивает, невероятно приятным, но, возможно, это только мое мнение.
Примечание
Почему двойная косая черта?
Вы могли заметить, что в функции init путь к файлу init предваряется двойной косой чертой:
Это не опечатка. По какой-то причине при наличии только одной косой черты Windows добавляет текущий локальный путь к пути скрипта, в результате чего жалуется, что файл не может быть найден (ну да). Добавление ещё одного слэша предотвращает такое поведение, а на других платформах игнорируется.
Остается функция update, задача которой — убедиться, что наша среда актуальна:
# Update the Docker environment
update () {
git pull \
&& build \
&& composer install \
&& artisan migrate \
&& yarn install \
&& start
}
Это удобный метод, который позволит получить репозиторий, собрать образы в случае изменения Docker-файлов, убедиться, что все изменения зависимостей применены и запущены новые миграции, а также перезапустить контейнеры, которым это необходимо (т. е. чей образ изменился).
Примечание
Управление отдельными репозиториями
Как я уже упоминал, в обычной установке вы, скорее всего, будете иметь среду Docker, бэкенд-приложение и фронтенд-приложение в отдельных репозиториях. Bash может помочь и в этой ситуации: если предположить, что папка src имеет git-ignored, а код размещен на GitHub, то функция извлечения репозиториев приложений может выглядеть следующим образом:
Заключение⚓︎
Целью этой серии является создание гибкой среды, которая облегчает нашу жизнь. Это означает, что пользовательский опыт должен быть максимально удобным, а необходимость запоминать десятки сложных команд не совсем подходит для этого.
Bash — это простой, но мощный инструмент, который в сочетании с Docker обеспечивает отличный опыт разработчика. После сегодняшней статьи взаимодействовать с нашей средой станет намного проще, а если вы вдруг забудете какую-то команду, то теперь для её повторения достаточно запустить команду demo без параметров.
Я изложил всё максимально просто, чтобы не загромождать статью, но очевидно, что из этого дуэта можно извлечь гораздо больше пользы. Можно упростить многие другие команды — просто адаптируйте слой под свои нужды.
В следующей части этой серии мы рассмотрим, как сгенерировать самоподписанный сертификат SSL/TLS, чтобы внедрить HTTPS в нашу среду.
Оригинальная статья: Docker for local web development, part 4: smoothing things out with Bash (English)