Один бінарник, щоб правити всіма: Laravel як один виконуваний файл
Навіщо робити блог розробника у 2026 році?
В епоху соціальних мереж та контент-платформ створення власного блогу розробника може здатися надмірністю. Але є щось унікально цінне у тому, щоб мати свій куточок інтернету — місце, де ти контролюєш стек, дизайн і контент. Жодних алгоритмів, що вирішують, хто бачить твої пости, жодного ризику платформи, і найкраще — сам блог стає частиною портфоліо.
Це історія того, як я створив і задеплоїв axvi.dev — двомовний блог розробника на сучасному PHP-стеку. Це не просто черговий туторіал "як задеплоїти Laravel". Це гайд з досягнення того, що раніше було неможливим у світі PHP — пакування всього додатку в один самодостатній бінарник без жодних зовнішніх залежностей.
Технічний стек
Перш ніж зануритися в деплой, подивимося, що ми деплоїмо:
- Laravel 12 — найновіша версія PHP-фреймворку
- Livewire 4 / Volt — реактивні UI-компоненти без написання JavaScript
- Filament 5 — красива адмін-панель для управління контентом
- FrankenPHP через Laravel Octane — сучасний PHP-сервер, побудований на Caddy
- PostgreSQL — база даних
- Redis — кешування, сесії та черги
- DaisyUI / Tailwind CSS — стилізація фронтенду з темами dracula та light
- Spatie Translatable — мультимовний контент (англійська + українська)
- Spatie Media Library — управління зображеннями
- honeystone/laravel-seo — SEO-метадані
- laravel-actions — бізнес-логіка, організована як класи дій
Чому FrankenPHP?
FrankenPHP — це сучасний PHP-сервер, написаний на Go, побудований поверх Caddy. На відміну від традиційних налаштувань PHP-FPM, FrankenPHP:
- Тримає Laravel-додаток у пам'яті (через Octane), усуваючи вартість завантаження при кожному запиті
- Працює одночасно як веб-сервер і PHP-рантайм — не потрібна комбінація nginx/Apache + PHP-FPM
- Підтримує HTTP/3, early hints та автоматичний HTTPS з коробки
- Може бути скомпільований у один статичний бінарник, що містить PHP, усі розширення та Caddy
Останній пункт і робить FrankenPHP справді революційним. Уявіть, що ви відправляєте весь PHP-додаток — фреймворк, залежності, веб-сервер і сам PHP — як один виконуваний файл. Жодного apt install php, жодного управління розширеннями, жодних конфліктів версій. Просто один бінарник, що працює будь-де.
Мультимовна архітектура
Блог підтримує англійську та українську мови через Spatie Translatable. Кожен елемент контенту — пости, категорії, теги — зберігає переклади як JSON у базі даних. Маршрути мають префікс локалі: /{locale}/blog, /{locale}/blog/{slug}.
Адмін-панель Filament робить управління перекладами безшовним — кожне перекладне поле має перемикач мови, тож можна писати контент обома мовами з однієї форми.
Налаштування сервера
Вибір інфраструктури
Я обрав Hetzner Cloud для хостингу. Для блогу розробника навіть їхній найменший інстанс — більш ніж достатньо:
- CX23: 2 vCPU, 4 ГБ RAM, 40 ГБ SSD — близько $3.49/місяць
- Локація: Нюрнберг, Німеччина — відмінна зв'язність по всій Європі
- ОС: Ubuntu 24.04 LTS
Початкова конфігурація сервера
Після створення сервера перші кроки завжди однакові:
1. Створення не-root користувача:
adduser deploy
usermod -aG sudo deploy
cp -r /root/.ssh /home/deploy/.ssh
chown -R deploy:deploy /home/deploy/.ssh
2. Налаштування файрволу:
sudo ufw allow 22
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable
3. Встановлення Docker:
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu noble stable" | \
sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker deploy
Просто, перевірено часом, нудно. Саме те, що потрібно для інфраструктури.
Архітектура Docker
Ось тут стає цікаво. Dockerfile використовує багатоетапну збірку з чотирма стейджами, кожен з яких відповідає за конкретну частину пайплайну збірки:
Стейдж 1: PHP-залежності
FROM php:8.5-cli-alpine AS composer-builder
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --ignore-platform-reqs --no-dev -a --no-scripts
COPY . .
RUN composer dump-autoload --optimize
Окремий стейдж для Composer-залежностей забезпечує чисту, продакшн-only директорію vendor з оптимізованим автолоадером. Цей стейдж йде першим, бо збірка фронтенду залежить від нього.
Стейдж 2: Збірка фронтенду
FROM node:22-alpine AS frontend-builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
COPY --from=composer-builder /app/vendor vendor
RUN npm run build
Цей стейдж компілює Tailwind CSS та JavaScript-ассети через Vite. Зверніть увагу на COPY --from=composer-builder — Tailwind CSS v4 з Vite-плагіном резолвить CSS-імпорти з директорії vendor/ (наприклад, теми підсвітки синтаксису з Composer-пакетів), тому vendor має бути доступним під час збірки фронтенду.
Стейдж 3: Збірка статичного бінарника
Тут відбувається магія:
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu AS static-builder
WORKDIR /go/src/app/dist/app
COPY --from=composer-builder /app .
COPY --from=frontend-builder /app/public/build public/build
WORKDIR /go/src/app/
RUN BUILDROOT=/go/src/app/dist/static-php-cli/buildroot \
&& cd dist/app && tar cf /go/src/app/app.tar . && cd /go/src/app \
&& sha256sum app.tar | cut -d' ' -f1 > app_checksum.txt \
&& export CGO_ENABLED=1 \
&& export CGO_CFLAGS="$($BUILDROOT/bin/php-config --includes) -I$BUILDROOT/include" \
&& ALL_LIBS=$(find $BUILDROOT/lib -name '*.a' -exec basename {} .a \; \
| sed 's/^lib/-l/' | tr '\n' ' ') \
&& export CGO_LDFLAGS="-L$BUILDROOT/lib \
-Wl,--start-group -lphp $ALL_LIBS -Wl,--end-group \
-lpthread -lm -ldl -lrt -lresolv -lutil -lstdc++" \
&& export PATH="$BUILDROOT/../pkgroot/x86_64-linux/go-xcaddy/bin:$PATH" \
&& XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'" \
xcaddy build --output dist/frankenphp-linux-x86_64 \
--with github.com/dunglas/frankenphp=/go/src/app \
--with github.com/dunglas/frankenphp/caddy=/go/src/app/caddy \
--with github.com/dunglas/caddy-cbrotli
Зверніть увагу — ми додаємо лише caddy-cbrotli (Brotli-стиснення) як додатковий модуль Caddy. Стандартні приклади FrankenPHP також включають Mercure (real-time push) та Vulcain (HTTP/2 preload), але якщо ваш додаток їх не використовує, вони лише збільшують розмір бінарника та спричиняють несподівані попапи дозволів у браузері. Тримайте бінарник легким — додавайте модулі тільки коли вони дійсно потрібні.
Ключовий інсайт: образ static-builder-gnu вже містить PHP та всі 60+ розширень, попередньо скомпільованих як libphp.a у buildroot. Стандартний підхід через build-static.sh ігнорує це й перекомпілює все з нуля (~20 хвилин). Замість цього ми викликаємо xcaddy напряму, лінкуючи з попередньо скомпільованими бібліотеками — це скорочує збірку до ~1-2 хвилин.
Флаги лінковщика --start-group/--end-group вирішують циклічні залежності між статичними бібліотеками, а ми динамічно збираємо всі .a файли з buildroot, щоб нічого не пропустити.
Результат? Один виконуваний файл frankenphp-linux-x86_64 (~240 МБ), що містить:
- Веб-сервер Caddy
- PHP 8.5 інтерпретатор з усіма 60+ розширеннями
- Весь код Laravel-додатку та vendor-залежності
- Фронтенд-ассети
Фінальний продакшн Docker-образ важить ~336 МБ. Для порівняння:
| Підхід | Орієнтовний розмір | Примітки |
|---|---|---|
| nginx + PHP-FPM | ~700-800 МБ | php:8.5-fpm (516 МБ) + nginx:alpine (62 МБ) + розширення + код додатку + vendor |
| FrankenPHP динамічний | ~400-450 МБ | dunglas/frankenphp:php8.5-alpine (203 МБ) + розширення (pdo_pgsql, redis, gd, intl...) + код додатку + vendor |
| FrankenPHP статичний (наш) | ~336 МБ | Все включено: PHP, Caddy, 60+ розширень, код додатку, фронтенд-ассети — нічого не потрібно встановлювати в рантаймі |
Підхід зі статичним бінарником не лише найлегший — він ще й найпередбачуваніший. Класична зв'язка nginx + PHP-FPM починається з 578 МБ лише базових образів і росте з кожним docker-php-ext-install. Динамічний образ FrankenPHP позбавляє від nginx, але все ще потребує встановлення розширень та управління залежностями. У нашому статичному образі все вже вбудовано — нічого не потрібно встановлювати, налаштовувати чи завантажувати в рантаймі.
Стейдж 4: Продакшн-образ
FROM debian:bookworm-slim AS static-prod
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd -r app && useradd -r -g app -d /app app
WORKDIR /app
COPY --from=static-builder /go/src/app/dist/frankenphp-linux-x86_64 \
/usr/local/bin/frankenphp
RUN mkdir -p storage/logs storage/framework/cache/data \
storage/framework/sessions storage/framework/views \
storage/app/public bootstrap/cache \
/data/caddy /config/caddy \
&& chown -R app:app storage bootstrap/cache /data /config
USER app
EXPOSE 80
CMD ["frankenphp", "php-cli", "artisan", "octane:frankenphp", \
"--host=0.0.0.0", "--port=80"]
Фінальний образ мінімальний: slim-базова ОС + один бінарник + записувані директорії для кешу та сесій Laravel. Зверніть увагу на директорії /data/caddy та /config/caddy — Caddy потребує їх для внутрішнього сховища та автозбереження конфігурації, і оскільки ми запускаємо процес від не-root користувача app, їх потрібно створити й призначити заздалегідь. Виділений не-root користувач app запускає процес. Жодної інсталяції PHP, Composer чи Node.js. Площа атаки мінімальна, і ламатися просто нічому.
Чому
debian:bookworm-slim, а не Alpine? Наш статичний бінарник скомпільований за допомогоюstatic-builder-gnu, який використовує glibc. Хоча бінарник статично злінкований, деякі системні виклики (DNS-резолюція черезgetaddrinfo, NSS-запити) можуть залежати від glibc під час виконання. Alpine використовує musl libc, що може спричинити неочевидні проблеми з DNS та локалями.bookworm-slimгарантує сумісність ціною ~80 МБ — прийнятний компроміс для надійності в production.
Збірка статичного бінарника: глибоке занурення
Збірка статичного PHP-бінарника — це подорож, і PHP 8.5 робить її ще захоплюючішою. Ось ключові інсайти, які я здобув.
Пастка build-static.sh
Офіційна документація FrankenPHP рекомендує використовувати build-static.sh зі змінними PHP_EXTENSIONS та PHP_EXTENSION_LIBS. Те, чого вона не повідомляє — образ static-builder-gnu вже містить усі 60+ розширень, попередньо скомпільованих як статичні бібліотеки в buildroot.
Можете перевірити самі:
docker run --rm dunglas/frankenphp:static-builder-gnu \
cat /go/src/app/dist/static-php-cli/buildroot/build-extensions.json
Проблема? build-static.sh викликає spc build, який завжди перекомпілює PHP з нуля незалежно від наявних артефактів buildroot. У static-php-cli немає підтримки інкрементальних збірок. Це означає ~20 хвилин, витрачених на кожну збірку на перекомпіляцію того, що вже є.
Рішення: пряма збірка через xcaddy
Замість build-static.sh ми викликаємо xcaddy напряму — інструмент збірки на Go, що створює бінарник Caddy з модулем FrankenPHP. Попередньо скомпільований libphp.a та всі бібліотеки розширень вже є в buildroot, тому потрібно лише:
- Запакувати додаток в
app.tar(механізм//go:embedу Go) - Встановити CGO-флаги з існуючого
php-config - Злінкувати всі статичні бібліотеки через
--start-group/--end-group
Результат: ~1-2 хвилини замість ~20 хвилин. Покращення у 10 разів.
Підводні камені статичної лінковки
При лінковці з попередньо скомпільованими статичними бібліотеками зверніть увагу на:
Відсутні бібліотеки у
php-config --libs— згенеровані флаги можуть не включати всі бібліотеки з buildroot (наприклад,libhashkit, потрібний дляlibmemcached). Безпечний підхід — динамічно зібрати всі.aфайли з buildroot.Циклічні залежності — статичні бібліотеки посилаються одна на одну складним чином. Лінковщик обробляє бібліотеки зліва направо і може пропустити символи. Обертання всього у
-Wl,--start-group ... -Wl,--end-groupзмушує лінковщик резолвити всі символи незалежно від порядку.Build constraints — Go-збірка потребує
CGO_ENABLED=1, щоб тегunixбув активним, інакше внутрішні пакети FrankenPHP не скомпілюються.
Поради для статичних збірок
Пропускайте
build-static.sh— викликайтеxcaddyнапряму з попередньо скомпільованими бібліотеками. Образstatic-builder-gnuвже має все скомпільоване — не витрачайте хвилини CI на перекомпіляцію.Використовуйте кеш GitHub Actions — директива
cache-from: type=ghaу docker/build-push-action кешує проміжні шари, тож незмінені стейджі не перебудовуються.GITHUB_TOKENне потрібен — при прямій збірці черезxcaddyGo-модулі завантажуються з Go proxy (не з GitHub API), тож обмеження запитів не є проблемою.Додайте
public/hotдо.dockerignore— коли запускаєтеnpm run devлокально, Vite створює файлpublic/hotз адресоюhttp://localhost:5173. Якщо цей файл потрапить у статичний бінарник черезCOPY . ., Laravel Vite helper вважатиме, що dev-сервер запущений у production, і завантажуватиме ассети з localhost замість скомпільованого build. Це спричиняє зламані стилі/скрипти та попап Chrome "Access other apps and services". Завжди виключайте його з контексту Docker-збірки.Створіть обгортку
phpта задайтеPHP_BINARY— статичний бінарник має лише виконуваний файлfrankenphp, але планувальник Laravel, обробник черги та SymfonyPhpExecutableFinderочікують бінарникphp. Без нього заплановані команди падають з помилкою'php': No such file or directory. Додайте це у ваш Dockerfile:
RUN printf '#!/bin/sh\nexec frankenphp php-cli "$@"\n' > /usr/local/bin/php \
&& chmod +x /usr/local/bin/php
ENV PHP_BINARY=/usr/local/bin/php
Змінна PHP_BINARY критично важлива — у вбудованій файловій системі PHP повідомляє PHP_BINARY як шлях до скрипту artisan замість справжнього виконуваного файлу PHP. Без неї планувальник не зможе коректно запускати команди. З такою конфігурацією ви також зможете використовувати звичні команди на кшталт docker exec app php artisan migrate замість багатослівного frankenphp php-cli artisan migrate.
Продакшн Docker Compose
Продакшн-налаштування використовує шість сервісів, оркестрованих Docker Compose:
services:
traefik:
image: traefik:v3
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
ports:
- "80:80"
- "443:443"
app:
image: ghcr.io/your-org/your-app:latest
healthcheck:
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/80'"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`${APP_DOMAIN}`)"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.routers.app-www.rule=Host(`www.${APP_DOMAIN}`)"
- "traefik.http.routers.app-www.middlewares=www-redirect"
- "traefik.http.middlewares.www-redirect.redirectregex.permanent=true"
worker:
image: ghcr.io/your-org/your-app:latest
command: ["frankenphp", "php-cli", "artisan", "queue:work"]
healthcheck:
test: ["CMD-SHELL", "php artisan queue:monitor redis:default,email --max=100"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
scheduler:
image: ghcr.io/your-org/your-app:latest
command: ["frankenphp", "php-cli", "artisan", "schedule:work"]
healthcheck:
test: ["CMD-SHELL", "php artisan schedule:list --no-interaction > /dev/null"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
db:
image: postgres:17
redis:
image: redis:alpine
Traefik як реверс-проксі
Traefik — невоспітаний герой цього налаштування. Він автоматично:
- Отримує та оновлює SSL-сертифікати Let's Encrypt через HTTP challenge
- Перенаправляє HTTP на HTTPS
- Перенаправляє
www.your-domain.comнаyour-domain.com - Маршрутизує трафік до правильного контейнера
- Забезпечує перевірку здоров'я
Все це без жодних конфігураційних файлів — все оголошується через Docker-лейбли. Більше ніякого ручного управління SSL-сертифікатами чи написання nginx-конфігів.
Health Checks без curl
Оскільки наш мінімальний продакшн-образ не містить curl чи wget, healthcheck для app використовує вбудований у Bash механізм /dev/tcp для перевірки відкритого порту. Це дозволяє уникнути встановлення додаткових пакетів лише заради health checking — образ залишається легким, а площа атаки — мінімальною.
Статичний бінарник у дії
Зі статичним бінарником запуск Artisan-команд виглядає трохи інакше:
# Замість: php artisan migrate
frankenphp php-cli artisan migrate
# Всередині Docker:
docker compose exec app frankenphp php-cli artisan tinker
Префікс frankenphp php-cli замінює php — бінарник містить PHP-інтерпретатор, тому окрема інсталяція PHP не потрібна. Один бінарник керує всім.
CI/CD пайплайн
GitHub Actions воркфлоу обробляє все автоматично при пуші в master:
jobs:
build:
steps:
- name: Build and push
uses: docker/build-push-action@v6
with:
target: static-prod
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
script: |
cd /opt/axvi
COMPOSE="docker compose -f docker-compose.prod.yml"
ARTISAN="$COMPOSE exec -T app frankenphp php-cli artisan"
$COMPOSE pull
$COMPOSE up -d
$ARTISAN storage:link --force
$ARTISAN migrate --force
$ARTISAN db:seed --force
$ARTISAN optimize:clear
$ARTISAN optimize
docker image prune -f
Пуш у master → збірка статичного бінарника → пуш Docker-образу в GHCR → SSH на сервер → pull і рестарт. Повністю автоматизований деплой без простою. З підходом прямого xcaddy весь пайплайн збірки завершується менш ніж за 3 хвилини.
Зверніть увагу на крок storage:link --force — оскільки статичний бінарник вбудовує файли додатку, симлінк public/storage не зберігається між перезбірками контейнера. Ця команда перестворює симлінк, що вказує на постійний Docker volume, забезпечуючи доступність завантажених медіа-файлів (наприклад, обкладинок постів, які керуються Spatie Media Library).
Оптимізації продуктивності
OPcache + JIT
Продакшн php.ini вмикає OPcache з JIT-компіляцією:
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.jit = 1255
opcache.jit_buffer_size = 128M
У поєднанні з Octane, що тримає додаток у пам'яті, це забезпечує час відповіді, що конкурує з Go та Node.js додатками.
Laravel Octane
FrankenPHP з Octane тримає Laravel-додаток завантаженим у пам'яті. Перший запит завантажує фреймворк, а наступні пропускають весь процес завантаження. Лише це може зменшити час відповіді на 50-70% порівняно з традиційним PHP-FPM.
Статичний бінарник робить це ще кращим — оскільки сам PHP скомпільований з усіма розширеннями, зв'язаними статично, немає накладних витрат на завантаження динамічних бібліотек. Все знаходиться в одному memory-mapped бінарнику.
Бенчмарки в реальних умовах
Щоб побачити, як це все працює на практиці, я запустив навантажувальні тести з локальної машини (Україна) на продакшн-сервер (Hetzner, Нюрнберг, Німеччина) за допомогою hey. Ці цифри включають повний мережевий round-trip (~3000 км), DNS-резолюцію та TLS-хендшейк — це те, що відчувають реальні користувачі, а не синтетичні localhost-бенчмарки.
Головна сторінка (/en) — 1000 запитів, 50 одночасних:
| Метрика | Значення |
|---|---|
| Запитів/сек | 35.5 |
| Середня затримка | 1.4с |
| P50 затримка | 1.3с |
| P99 затримка | 2.0с |
| Успішність | 100% |
Сторінка блог-посту (/en/blog/...) — 500 запитів, 25 одночасних:
| Метрика | Значення |
|---|---|
| Запитів/сек | 20.8 |
| Середня затримка | 1.2с |
| P50 затримка | 1.2с |
| P99 затримка | 2.0с |
| Успішність | 100% |
Стрес-тест — шукаємо межі на сервері за $3.49/місяць (2 vCPU, 4 ГБ RAM):
| Одночасних з'єднань | Успішність | RPS | Середня затримка | P99 затримка |
|---|---|---|---|---|
| 50 | 100% | 35 | 1.4с | 2.0с |
| 100 | 100% | 30 | 2.9с | 4.5с |
| 150 | 90% | 33 | 3.8с | 5.6с |
| 200 | 80% | 31 | 5.1с | 7.7с |
Сервер впевнено тримає 100 одночасних з'єднань без жодної помилки та підтримує ~30-35 запитів/секунду. Втрати починаються лише при 150+ одночасних з'єднаннях. Цифри затримки включають ~500мс мережевих накладних витрат (DNS, TLS, передача даних через Європу) — фактичний час відповіді сервера становить близько 500-600мс.
Для блогу розробника на найдешевшому інстансі Hetzner — це серйозний запас. Потрібні тисячі одночасних читачів, щоб це налаштування хоча б напружилось.
Чому статичний бінарник того вартий
Статичний бінарник FrankenPHP — це зміна парадигми у PHP-деплої:
- Нуль залежностей рантайму — жодної інсталяції PHP для управління, жодних конфліктів розширень, жодних розбіжностей версій між середовищами
- Іммутабельні деплої — той самий бінарник працює в CI, staging і production. Що тестуєш — те й відправляєш
- Мінімальна площа атаки — жодного пакетного менеджера, жодних shell-утиліт, жодних зайвих системних бібліотек. Тільки ваш додаток
- Самодостатність — бінарник включає PHP, Caddy та весь ваш додаток. Скопіюйте його на будь-яку Linux-машину — і він працює
- HTTP/3 + QUIC з коробки — з скомпільованими ngtcp2 та nghttp3 ваш додаток нативно підтримує найновіші протоколи
З підходом прямого xcaddy збірка така ж швидка, як стандартний Docker-образ — менше 3 хвилин загалом. Ваше продакшн-середовище отримує куленепробивний, мінімальний артефакт деплою, якого неможливо досягти з традиційними PHP-налаштуваннями.
Висновок
Деплоїти Laravel-блог у 2026 році зі статичним бінарником FrankenPHP — це майбутнє PHP-деплою. Поєднання статичних збірок FrankenPHP, автоматичного SSL від Traefik та CI/CD через GitHub Actions створює пайплайн деплою, який є одночасно потужним та елегантним.
Блог працює на axvi.dev. Весь стек крутиться на сервері Hetzner за $3.49/місяць, легко обробляє трафік і деплоїться автоматично при кожному git push. Один бінарник, один сервер, нуль компромісів.
Маєте питання про це налаштування? Знайдіть мене на GitHub, LinkedIn або X.