3. Docker for laravel octane, queue(horizon) and scheduling, PostgresSQL. Nginx proxy. Local development
Відео версія
(YouTube - для зворотнього зв'язку)
Цей гайд для локальної розробки. Не використовуйте його в продакшені.
TL; DR
Docker - обов'язковий. Особливо для php розробників
Не використовуйте laravel sail or laradock. Хіба що для натхнення
Використовуйте лише фіксовані версії docker зображень. Так з патчем.
Linux alpine - гарний вибір для контейнеризації, маленький, швидкий та безпечний.
Використовуйте nginx як посередник навіть для локальної розробки
Не забувай про
.dockerignore
Ніколи не використовую докер мережу за замовчуванням
docker compose бачить змінні в .env файлі
Використовуй Postgres замість Mysql
Docker нічого не зберігає. Використовуй volume для збереження даних.
Знайте docker
Для бекенд розробника знання Docker - обов'язкове. Прийміть це, розберіться і живіть далі.
Якщо у вас більше одного контейнера використовуйте docker compose.
docker-compose - розроблявся як окреме рішення і переросло в розширення для докера (docker compose без дефіса). Розширення офіційне з виправленням всіх ризиків оригінала, і більшість обговорень про те що compose небезпечно використовувати в продакшені наразі не актуально.
laravel sail та laradoc - готові рішення для контейнеризації laravel проєкту з коробки.
Не рекомендую їх використовувати з коробки. Причин багато:
- Рішення "з коробки" не дають тобі й команді зрозуміти сервісні підходи. Шукаючи рішення ви не думаєте що можна взяти умовний сервіс на умовному каболі й просто використовувати.
- Як показує практика ці рішення частенько тягнуть в продакшен. Або копіюють в сліпу і тягнуть в прод. А якщо ні, то локальне середовище відрізняється від прода - що по факту невілює плюси докера й ставить під питання стабільність вашого коду
Використовуйте фіксовані версії контейнерів!
- Починаються проблеми з оновленнями та розширеннями. Версійність докер імеджей працює по тому ж принципу, що й версійність пакетних менеджерів. Якщо ви не вказали патч версію - версія поставиться остання. А в цих пакетах патч версія ніколи не вказується. А значить версія у нового розробника й у розробника який почав робити тиждень тому - можуть бути різні. Ну і звісно в проді у вас також версія ніхто не знає яка. І ви не можете з впевненістю сказати, а чи пофікщена та вразливість php про яку всі зараз говорять, чи ні.
- Оптимізація і безпека. Тут все просто. Розгортати контейнери в убунті не ок. Вона важка, вона має багато залежностей які вам не треба і які потенційно можуть мати вразливості в безпеці. Про розмір убунти також думайте при деплої, кожен раз качати її й розгортати не ок. Ми й так холіваримо що бистріше yarn чи нмп... А потім ставимо їх на убунту... Натомість раджу Linux alpine. Маленький, безречний і швидкий.
Linux alpine - small, secure good solutions for you containers.
Але це все ж таки це позитивні проєкти, якщо
- Ти новачок і вчишся саме кодити - використовуйте. Хоча краще навчиться налаштовувати локальну машину
- Треба щось тестувати, або показати. Без думки що над цим будемо
- Треба щось підглянути. Лізьте у вихідний код і дивиться, там круті ідеї та практики. Скоріше за все коректніші ніж ті що ви побачите у мене.
Поради по docker compose
Тут не буду розписувати документацію. Набір загальних рекомендацій, про які, на мій суб'єктивний досвід, часто забувають розробники
- Docker нічого не зберігає, всі дані існують в ньому лише поки він працює. Використовуйте "іменні сховища" (top level volumes) Це дасть змогу зберігати дані сервісу (або шейрети між сервісами) не задумуючись де їх зберігати.
- docker compose автоматично створює локальну мережу і включає туди всі сервіси. Не використовуйте її. Обмежуйте видимість кожного сервісу. Сервіс має мати доступ лише до тих сервісів з якими він взаємодіє. Так ви мінімізуєте ураження від злому якого сервісу. І робіть це обов'язково навіть при локальній розробці. Людина що займеться деплоєм (умовний DevOps) навряд буде розбиратися що і як взаємодіє у вас в коді, і скопіює 1 в 1 ваш локальний сетап.
healthcheck
- дозволяє додати команду перевірки запущеного сервісу. Важливо для наступного пункту, а для локальної розробки дозволяє повністю абстрагуватися від хоста, наприклад для інсталяції залежностей. Контейнер піднятий без залежності - дозволяє їх встановити, якщо все встановлено - відмітитися якhealthy
. І звісно не полагайтась на healthcheck як на трекінговий показник, якщо щось упало в процесі роботи докер цього не покаже. А самі функції звідти нормальний DevOps використає як треба.depends_on: condition: service_healthy
- запускає сервіс лише після повного запуску сервісу від якого залежить. Для локальної роботи не принципово, а в продакшені допоможе позбавити ваш баг трекер купи помилок які відбулись, тому що сервіси стартонули одночасно, а запрацювали ні.- yaml reference
&some_name
+ yaml merge>>:
дозволять легко розгортати сервіси на основі однакових імейджей, не дублюючі код - Docker compose працює з .env файлом - користуймося цим.
- Відкривайте лише необхідні порти на host. Якщо треба відкрити порт в середені мережі -
expose
DB service
Якщо не знаєте навіщо вам щось інше - беріть реляційну. NoSql графові, колонкові й всі інші - якщо знаєте навіщо вони проєкту. Якщо ви все ще на MysSql - кидайте це діло. Postgres неймовірно крутий, навіть якщо ви новачок - раджу починати одразу на ньому. В моїй збірці pogtgis - це postgres з встановленим плагіном на роботу з геолокацією. Планів на геолокацію багато, якщо вам не потрібно - просто postgres. Плагіни завжди можна поставити опісля.
db:
image: postgis/postgis:16-3.4-alpine
restart: unless-stopped
container_name: bh-db
environment:
POSTGRES_DB: ${DB_DATABASE}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- 5432:5432
volumes:
- db_data:/var/lib/postgresql/data
networks:
- bh-db-network
healthcheck:
test: pg_isready -U ${DB_USERNAME} -d ${DB_DATABASE}
interval: 5s
timeout: 10s
retries: 20
DB_DATABASE
,DB_USERNAME
таDB_PASSWORD
- docker compose прочитає з файлу конфігурації .env. База даних створиться автоматично з цими параметрами при першому запуску.container_name: bh-db
-bh-db
буде використовуватися як хост. Саме його використовуємо для підключення laraveltest: pg_isready -U ${DB_USERNAME} -d ${DB_DATABASE}
- перевіряємо чи БД успішно запущена. Не стикався з проблемами, але плагіни, міграція по версіях можуть впливати.
PHP service
Контейнер з php (APP). Тут на вибір.
- PhpFPM - перевірено та безпечно.
- Nginx-unit - колись хайпував, рекламувався як топ рішення, я особисто ніколи не зустрічав в практиці, тому на ваш страх і ризик.
- І звісно laravel octane. Це мій вибір на проєкт, не через швидкість php fpm з opcache впевнений дасть +- ту саму швидкість. Я ж беру з необхідності розвиватися, як спеціаліст я повинен знати особливості. А також маю ідеї проєктів для яких саме octane буде ок. Тому це чисто суб'єктивний вибір!
Якщо ваш вибір octane, то треба зробити ще один вибір, власне рушій на якому октан буде працювати. FrankenPHP, RoadRunner, Swoole.
FrankenPHP, на момент прийняття рішення, - нова розробка, а бути альфатестером бажання нема. Між RoadRunner та Swoole - мій суб'єктивний вибір
Swoole, тільки через його унікальні можливості, можливо в майбутньому зміню думку.
З Swoole знову треба зробити вибір - open swoole або звичайний, поки великої різниці нема, є внутрішні розбіжності в командах, але для потреб проєкту вони не значні.
Офіційний докер імейдж можна знайти тут.
І оскільки це php імейдж з величейзним шансом знадобиться ставити на нього різного роду php розширення.
В стандартній ситуації я б радив створити Dockerfile де ставив би розширення поверх імейжду
FROM phpswoole/swoole...
Але прочитавши документацію і глянувши у вихідний код, можна побачити що там вже встановлено багато чого. Багато чого, і багато чого зайвого. Для мене це MySql. Якщо працювати поступово, то redis, socket також треба видалити.
Тому за основу беремо офіційний вихідний код. Видаляємо те що нам не треба. Ставимо залежності яких не вистачає. Оновлюємо, що можемо оновити.
FROM php:8.3.6-cli-alpine3.18
COPY --from=composer:2.7.4 /usr/bin/composer /usr/bin/
RUN \
set -ex && \
apk update && \
apk add --no-cache libstdc++ libpq && \
apk add --no-cache --virtual .build-deps $PHPIZE_DEPS curl-dev linux-headers postgresql-dev openssl-dev pcre-dev pcre2-dev zlib-dev && \
apk add --no-cache supervisor && \
apk add --no-cache supercronic && \
pecl channel-update pecl.php.net && \
docker-php-ext-install sockets && \
pecl install redis && \
docker-php-ext-enable redis && \
docker-php-source extract && \
mkdir /usr/src/php/ext/swoole && \
curl -sfL https://github.com/swoole/swoole-src/archive/v5.1.2.tar.gz -o swoole.tar.gz && \
tar xfz swoole.tar.gz --strip-components=1 -C /usr/src/php/ext/swoole && \
docker-php-ext-configure swoole \
--enable-swoole-pgsql \
--enable-openssl \
--enable-sockets --enable-swoole-curl && \
docker-php-ext-install -j$(nproc) swoole && \
docker-php-ext-configure pcntl --enable-pcntl && \
docker-php-ext-install pcntl && \
rm -f swoole.tar.gz && \
docker-php-source delete && \
apk del .build-deps
RUN apk add --no-cache nodejs npm git
COPY docker/php/supervisord/supervisord.conf /etc/
EXPOSE 8000
WORKDIR "/var/www/"
ENTRYPOINT ["supervisord", "--nodaemon", "--configuration", "/etc/supervisord.conf"]
Тут зверніть увагу на
EXPOSE 8000
- робить порт 8000 доступним для інших сервісів в рамках мережі. Для хоста цей порт закритий.pcntl
- розширення для паралельної роботи процесів. Обов'язковий для Laravel OctaneRUN apk add --no-cache nodejs npm git
- noda необхідна для для watch mod Laravel Octane. Не ставте її в prod версії. Git дозволить запускать commitlint та branch name validator з контейнеру. Так ми на 100% незалежні від середовища розробника. Також не ставимо в прод!supervisord
таENTRYPOINT
- тут треба розуміння докер працює доки активний основний процес докеру. в нашому випадку цей процесphp artisan ocrane:start
але нагадаю ми розгортаємо проєкт локально - і не хочемо залежати від локального середовища розробника, а значить розробник повинен мати можливість маніпулювати пакетами, встановлювати/оновлювати на піднятому сервісі. Тому головною командою у нас supervisord, а вже в середні нього запускаємо команду.
Supervisor - утиліта для керування процесами. Запускає, перезапускає, слідить за виконанням.
[supervisord]
nodaemon=true
logfile=/var/www/storage/logs/supervisord_%(ENV_PROCESS)s.log
pidfile=/var/www/storage/logs/supervisord_%(ENV_PROCESS)s.pid
[program:php]
directory=/var/www/
command: %(ENV_COMMAND)s
startretries: 20
Тут зверніть увагу
nodaemon=true
- не можна запускати супервізор в докері в режимі демона. Він поверне код запуску і контейнер зупиниться.logfile
,pidfile
- має сенс перенаправити на директорі з логами. За замовчуванням він буде генеруватися в кореневій директорії й заважатиcommand: %(ENV_COMMAND)s
- команда яку хочемо запустити. Для app сервісу цеphp artisan octane:start
. У моєму випадку є потребність передати команду через змінні середовища. тому тут%(ENV_COMMAND)s
. Це стане зрозумілим на сервісах черг.startretries
- кількість спроб запустити команду. Кожна спроба йде із затримкою в кількість минулих спроб секунд. Тобто супервізор буде пробувати запустити команду кожні 1,2,3,6... 20 секунд перш ніж видасть помилку. Це не ідеально, але на жаль я не знайшов нормальної реалізації таймаута в супервізорі, тому поки так
app: &app
build:
context: ./
dockerfile: docker/php/Dockerfile
user: ${UID}:${GID}
restart: unless-stopped
container_name: bh-app
environment:
COMMAND: php artisan octane:start --watch --host=0.0.0.0 --port=8000
PROCESS: app
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- ./:/var/www
- bh_app_composer_data:/.composer
- bh_app_npm_data:/.npm
networks:
- bh-db-network
- bh-webserver-network
- bh-redis-network
healthcheck:
test: curl -s http://localhost:8000/up >/dev/null || exit 1
interval: 5s
timeout: 10s
retries: 20
Тут важливо:
build: ...
- app зображення ми збираємо локально (на данний момент). Всі інші - беремо готові.&app
- якір|alias|reference на сервіс (або блок коду). Використовуються в Queue|Schedule сервісахuser: ${UID}:${GID}
- запускаємо контейнер з правами локального користувача. Всі файли що створяться в сервісі, зможете спокійно редагувати на хості. І, будь ласка, будь ласка, думайте коли бачите рішення в інтернеті. права777 ЦЕ НЕ РІШЕННЯ
.
echo UID=$(id -u) >> .env
echo GID=$(id -g) >> .env
volumes: - ./:/var/www
- всі файли (окрім.dockerignore
) з кореневого каталогу хосту в ROOT_DIR контейнера. Основа дев збірки, щось змінюємо - контейнер це бачитьCOMMAND: php artisan octane:start --watch --host=0.0.0.0 --port=8000
- команда яку запустимо в supervisor. Обовязково вкажіть хост, бо хост за замовчуванням не хоститься в мережах докераPROCESS: app
- назва процесу, використовується для ідентифікації лог файлів супервізораtest: curl -s http://localhost:8000/up >/dev/null || exit 1
- "розпіарена" функція up в laravel 11. Ось тут вони й потрібні.bh_app_composer_data:/.composer
таbh_app_npm_data:/.npm
- зберігаємо кєш composer та npm. Не обов'язково, але зручно і дозволяє зупиняти контейнер без втрати кешу, та необхідності щось робити з правами кеш директорій.
Web server (webserver)
Тут важливо - так можна працювати без нього, особливо локально. АЛЕ на проді він обовзязковий, а значить про всі проблеми взаємодії, конфігурації й т.д. краще знати на етапі розробці. Конкуренту nginx тут немає, apache навіть не розглядається вже купу років.
Конфігурацію беру прямо з документації laravel octane. Логіка проста - всю статику віддає nginx для всіх запитів - octane. Але все ж треба підлаштувати під себе:
- Видалив
favicon
таrobot.txt
. API вони не потрібні resolver 127.0.0.11
- це стандартний DNS докеру, без нього nginxproxy_pass http://bh-app:8000$suffix
- тут наш app container name
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_tokens off;
root /var/www/public;
index index.php;
charset utf-8;
location /index.php {
try_files /not_exists @octane;
}
location / {
try_files $uri $uri/ @octane;
}
access_log /var/log/nginx/nginx-access.log;
error_log /var/log/nginx/nginx-error.log error;
error_page 404 /index.php;
resolver 127.0.0.11;
location @octane {
set $suffix "";
if ($uri = /index.php) {
set $suffix ?$query_string;
}
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header SERVER_PORT $server_port;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://bh-app:8000$suffix;
}
}
Redis
Кєш сервіс. Для laravel однозначний вибір це Redis. Він же драйвер бродкасту, він же драйвер для черг. Швидка key-value база даних.
Тут все просто. Єдине що раджу запускати одразу з паролем, знову ж таки щоб відповідальний за реліз не забув. І при необхідності команда могла б відкрити порт у світ з меншими ризики.
redis:
image: redis:7.2.4-alpine
restart: unless-stopped
container_name: bh-redis
command:
- 'redis-server'
- '--requirepass ${REDIS_PASSWORD}'
volumes:
- redis_data:/data
networks:
- bh-redis-network
healthcheck:
test: redis-cli ping
interval: 5s
timeout: 10s
retries: 20
Queue service
Тут треба сказати. Рекомендую не ставити все й одразу. Якщо в проєкті нема черг, запитів за розкладом, або умовно потребі в кеші - не реалізуйте їх.
Так з великою ймовірністю ви активуєте ці сервіси. Але часто бачу налаштовані сервіси які не використовуються в проєкті. Створив їх хтось з упевненістю, що це обов'язково будуть використовувати... А не видаляються, тому що всім страшно, бо ніхто не зна де ж воно використовується.
І друге - я розумію що і queue і schedule сервіси можна було б запустити всередині app, тим же супервізором. Але в докері є принцип один сервіс - один процес. Це дає змогу обмежити ресурси на якийсь із сервісів (наприклад черг), слідкувати за його станом тощо.
Ок повертаємося до черг. Тут laravel має готовий пакет Laravel Horizon. Його і рекомендую ставити.
Лише одна порада(на даному етапі) - в документації радять на hook composer update поставити команду публікації ресурсів horizon.
Я ж раджу додати public/vendor
до .gitignore
і додати команду публікації також і в install.
Це ж саме рекомендую для всіх подібних сервісів. Тому що цінності цей код не несе, він завжди ставиться з пакетами,
він завжди вимагає публікації, а ось відправляти купу генерованого коду на ревью не ок, тим більше по-хорошому,
його треба ревьюшити, бо впихнути якийсь свій скриптик на horizon view знаючи що його ніхто не перевірить ...
{
"post-install-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
]
}
horizon:
<<: *app
container_name: bh-horizon
environment:
COMMAND: php artisan horizon
PROCESS: horizon
networks:
- bh-db-network
- bh-redis-network
healthcheck:
test: php artisan horizon:status | grep -q 'is running' # TODO try spatie LARAVEL-HEALTH
interval: 5s
timeout: 10s
retries: 20
<<: *app
- merge з&app
всі поля дублюються з&app
, ті що описані нижче - перевизначаються. Тобто тут береться той же самий імейдж, правила рестарту, залежності тощо. Але назва контейнер, environment, networks, та healthcheck - власні.test: php artisan horizon:status | grep -q 'is running
- сумнівне рішення, краще використати щось типу Але це в майбутньому цим займусь.
Schedule service
Сервіс для планування завдань (schedule). Тут, зазвичай по крону кожну хвилину запускаємо команду.
* * * * * cd /var/www && php artisan schedule:run >> /dev/null 2>&1
Тут одна особливість. Замість звичайного крона - раджу supercronic Це крон заточений під роботу в контейнері, дозволяє використовувати змінні середовища, керувати виводом результату, і працювати як активний процес.
Особисто мене більше цікавить можливість інтеграція з Sentry. Але про це іншим разом.
schedule:
<<: *app
container_name: bh-schedule
environment:
COMMAND: supercronic -quiet /var/www/docker/php/schedule/crontab
PROCESS: schedule
networks:
- bh-db-network
- bh-redis-network
healthcheck:
test: supercronic -test /var/www/docker/php/schedule/crontab | grep -q 'is valid' # TODO try spatie LARAVEL-HEALTH
interval: 5s
timeout: 10s
retries: 2
Dockerignore
Про .dockerignore
забувають дуже часто. Пам'ятаймо - все що не потрібно для роботи докеру - не пхаємо туди.
В продакшен контейнер також додавайте тести, stubs, mocks і тд. Не треба це тягти в прод.
**.git
.idea
**.gitignore
**.gitattributes
node_modules/
public/hot/
public/storage/
storage/*.key
storage/app/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
storage/logs/*
.env.example
.phpunit.result.cache
npm-debug.log
yarn-error.log
Dockerfile
docker-compose.yml
branchlint.config.json
commitlint.config.js
README.md
.husky
.styleci.yml
.editorconfig
.phpstorm.meta.php
_ide_helper.php
_ide_helper_models.php
.gitlab-ci.yml
psysh
setup-local.sh
_ide_helper_actions.php
Readme та ENV
Ця інструкція - єдина яку прочитає ваша команда. Така реальність 🥲
Маленька порада - тип команду в MD форматі робіть sh. Це дасть змогу просто проклікати команди прямо з IDE
## Setup local
1) Copy environments
```sh
cp .env.example .env
```
2) Add current user identity data to environments
```sh
echo UID=$(id -u) >> .env
echo GID=$(id -g) >> .env
```
3) Run docker
```sh
docker compose up -d --build
```
4) Make cache dirs for package managers
```sh
docker compose exec -u root app install -o $(id -u) -g $(id -g) -d "/.npm" &&
docker compose exec -u root app install -o $(id -u) -g $(id -g) -d "/.composer"
```
5) Install composer dependency
```sh
docker compose exec app composer install
```
6) Install npm dependency
```sh
docker compose exec app npm i
```
7) Generate app key
```sh
docker compose exec app php artisan key:generate
```
8) #if services still unhealthy - restart docker
```sh
docker compose up -d --build
```
### Entry points
* [http://localhost/](http://localhost/) - application
* [http://localhost/horizon](http://localhost/horizon) - queue manager
Тут один момент
docker compose exec app npm i
Встановлюємо npm залежності також в контейнері. Зазвичай для бекенду оновлюються вони майже ніколи. А ось зміни lock файлу можуть прилетіти залежно від версії на локальній машині. Їх звісно ж відправлять на ревью, що не ок, потребує зайвої уваги.
І не забуваймо синхронізувати .env example
DB_CONNECTION=pgsql
DB_HOST=bh-db
DB_PORT=5432
DB_DATABASE=band_h
DB_USERNAME=bh_db_user
DB_PASSWORD=bh_db_password
BROADCAST_DRIVER=redis
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=bh-redis
REDIS_PASSWORD=redis_password
OCTANE_SERVER=swoole
І тепер можна запускати лінтери в контейнері:
docker exec -t bh-app npx validate-branch-name
docker exec -t bh-app npx --no -- commitlint --edit $1
Виражаю повагу тим хто дочитав до сюда і подумав: "Автор пів дня розпинався про версійність у всіх все однакове. А кінець кінцем білдить app сервіс у кожного сервіси заново з нуля". Дивіться наступний урок.