работа с контейнерами

This commit is contained in:
2026-08-03 00:09:50 +04:00
parent 6c18cad816
commit c25304919c
13 changed files with 599 additions and 4 deletions
+22
View File
@@ -0,0 +1,22 @@
.git
.gitignore
.env
.env.*
!.env.example
.idea
.vscode
node_modules
vendor
public/build
public/hot
public/storage
bootstrap/cache/*.php
storage/*.key
storage/backups
storage/logs/*
storage/framework/cache/data/*
storage/framework/sessions/*
storage/framework/views/*
tests
compose*.yaml
README.md
+17
View File
@@ -77,6 +77,23 @@ chmod +x scripts/deploy-ubuntu.sh
./scripts/deploy-ubuntu.sh --skip-cache
```
## Production без Sail
Для production используется отдельный стек `compose.prod.yaml`:
- `web` — непривилегированный nginx со статикой;
- `app` — PHP-FPM с кодом и Composer-зависимостями внутри образа;
- `mysql` — постоянная база на томе `sail-mysql`.
Node.js, Playwright, Xdebug, dev-зависимости Composer и bind-mount исходного кода в production-образы не входят.
```bash
chmod +x scripts/deploy-production.sh
./scripts/deploy-production.sh
```
Скрипт сначала собирает и проверяет новый PHP-FPM, выполняет миграции и прогрев кэша, а затем переключает порт с legacy Sail-контейнера на production nginx. Если HTTP-проверка не проходит, legacy-контейнер запускается обратно.
## Как это связано с AzerothCore
По умолчанию Sail-контейнер подключается к auth-базе core-сервера так:
+105
View File
@@ -0,0 +1,105 @@
services:
app:
build:
context: .
dockerfile: docker/production/Dockerfile
target: app
args:
APP_UID: '${WWWUSER:-1000}'
APP_GID: '${WWWGROUP:-1000}'
image: moonwell-web-app:production
restart: unless-stopped
init: true
env_file:
- .env
environment:
APP_ENV: production
APP_DEBUG: 'false'
DB_HOST: mysql
DB_PORT: 3306
volumes:
- './storage:/var/www/html/storage'
tmpfs:
- '/var/www/html/bootstrap/cache:uid=${WWWUSER:-1000},gid=${WWWGROUP:-1000},mode=0770'
networks:
- sail
depends_on:
mysql:
condition: service_healthy
healthcheck:
test:
- CMD-SHELL
- 'SCRIPT_NAME=/healthz SCRIPT_FILENAME=/healthz REQUEST_METHOD=GET cgi-fcgi -bind -connect 127.0.0.1:9000 2>/dev/null | grep -q pong'
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
web:
build:
context: .
dockerfile: docker/production/Dockerfile
target: web
args:
APP_UID: '${WWWUSER:-1000}'
APP_GID: '${WWWGROUP:-1000}'
image: moonwell-web-nginx:production
restart: unless-stopped
init: true
ports:
- '127.0.0.1:${APP_PORT:-8080}:8080'
volumes:
- './storage/app/public:/var/www/html/public/storage:ro'
networks:
- sail
depends_on:
app:
condition: service_healthy
healthcheck:
test: ['CMD', 'wget', '--quiet', '--tries=1', '--spider', 'http://127.0.0.1:8080/healthz']
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
mysql:
image: mysql:8.4
restart: unless-stopped
ports:
- '${FORWARD_DB_PORT:-3307}:3306'
environment:
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
MYSQL_ROOT_HOST: '%'
MYSQL_DATABASE: '${DB_DATABASE}'
MYSQL_USER: '${DB_USERNAME}'
MYSQL_PASSWORD: '${DB_PASSWORD}'
MYSQL_ALLOW_EMPTY_PASSWORD: 1
MYSQL_EXTRA_OPTIONS: '${MYSQL_EXTRA_OPTIONS:-}'
volumes:
- 'sail-mysql:/var/lib/mysql'
- './docker/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
networks:
- sail
healthcheck:
test:
- CMD
- mysqladmin
- ping
- '-p${DB_PASSWORD}'
retries: 3
timeout: 5s
networks:
sail:
driver: bridge
volumes:
sail-mysql:
driver: local
+6
View File
@@ -24,6 +24,12 @@ services:
- sail
depends_on:
- mysql
healthcheck:
test: ['CMD', 'curl', '--fail', '--silent', 'http://127.0.0.1/healthz']
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
mysql:
image: 'mysql:8.4'
restart: unless-stopped
+116
View File
@@ -0,0 +1,116 @@
# syntax=docker/dockerfile:1
FROM php:8.3-fpm-bookworm AS php-base
ARG APP_UID=1000
ARG APP_GID=1000
ENV APP_ENV=production \
APP_DEBUG=false \
COMPOSER_ALLOW_SUPERUSER=1
WORKDIR /var/www/html
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libfcgi-bin \
libfreetype6-dev \
libicu-dev \
libjpeg62-turbo-dev \
libpng-dev \
libzip-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j"$(nproc)" \
bcmath \
exif \
gd \
intl \
pcntl \
pdo_mysql \
sockets \
zip \
&& apt-mark manual \
libfcgi-bin \
libfreetype6 \
libicu72 \
libjpeg62-turbo \
libpng16-16 \
libzip4 \
&& apt-get purge -y --auto-remove \
libfreetype6-dev \
libicu-dev \
libjpeg62-turbo-dev \
libpng-dev \
libzip-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --gid "$APP_GID" app \
&& useradd --uid "$APP_UID" --gid "$APP_GID" --create-home --shell /usr/sbin/nologin app
COPY docker/production/php.ini /usr/local/etc/php/conf.d/99-production.ini
COPY docker/production/php-fpm-pool.conf /usr/local/etc/php-fpm.d/zz-production.conf
FROM php-base AS vendor
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
COPY . .
RUN composer install \
--no-dev \
--prefer-dist \
--optimize-autoloader \
--no-interaction \
--no-progress \
&& composer clear-cache
FROM node:24-bookworm-slim AS frontend
WORKDIR /var/www/html
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
COPY resources ./resources
COPY public ./public
COPY vite.config.* ./
ARG VITE_APP_NAME=Moonwell
ENV VITE_APP_NAME="$VITE_APP_NAME"
RUN npm run build
FROM php-base AS app
COPY --from=vendor --chown=app:app /var/www/html /var/www/html
COPY --from=frontend --chown=app:app /var/www/html/public/build /var/www/html/public/build
RUN mkdir -p \
bootstrap/cache \
storage/app/public \
storage/framework/cache/data \
storage/framework/sessions \
storage/framework/views \
storage/logs \
&& chown -R app:app bootstrap/cache storage
EXPOSE 9000
CMD ["php-fpm", "--nodaemonize"]
FROM nginx:1.27-alpine AS web
ARG APP_UID=1000
ARG APP_GID=1000
RUN addgroup -S -g "$APP_GID" app \
&& adduser -S -D -H -u "$APP_UID" -G app app
COPY docker/production/nginx.conf /etc/nginx/nginx.conf
COPY --from=vendor --chown=app:app /var/www/html/public /var/www/html/public
COPY --from=frontend --chown=app:app /var/www/html/public/build /var/www/html/public/build
USER app
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
+60
View File
@@ -0,0 +1,60 @@
worker_processes auto;
pid /tmp/nginx.pid;
error_log /dev/stderr warn;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
client_max_body_size 100m;
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
server {
listen 8080 default_server;
server_name _;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /healthz {
allow 127.0.0.1;
deny all;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /healthz;
fastcgi_pass app:9000;
}
location = /index.php {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
fastcgi_param HTTP_PROXY "";
fastcgi_pass app:9000;
fastcgi_read_timeout 60s;
}
location ~ \.php$ {
return 404;
}
location ~ /\. {
deny all;
}
}
}
+23
View File
@@ -0,0 +1,23 @@
[www]
user = app
group = app
listen = 0.0.0.0:9000
pm = dynamic
pm.max_children = 8
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 4
pm.max_requests = 500
request_terminate_timeout = 60s
request_slowlog_timeout = 10s
slowlog = /proc/self/fd/2
ping.path = /healthz
ping.response = pong
clear_env = no
catch_workers_output = yes
decorate_workers_output = no
+22
View File
@@ -0,0 +1,22 @@
[PHP]
expose_php = Off
display_errors = Off
display_startup_errors = Off
log_errors = On
memory_limit = 256M
max_execution_time = 60
post_max_size = 100M
upload_max_filesize = 100M
variables_order = EGPCS
[Date]
date.timezone = UTC
[opcache]
opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.save_comments = 1
+8
View File
@@ -56,6 +56,11 @@ RUN apt-get update && apt-get upgrade -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN apt-get update \
&& apt-get install -y nginx php8.3-fpm \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.3
RUN userdel -r ubuntu
@@ -65,7 +70,10 @@ RUN git config --global --add safe.directory /var/www/html
COPY start-container /usr/local/bin/start-container
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY nginx.conf /etc/nginx/nginx.conf
COPY php-fpm-pool.conf /etc/php/8.3/fpm/pool.d/www.conf
COPY php.ini /etc/php/8.3/cli/conf.d/99-sail.ini
COPY php.ini /etc/php/8.3/fpm/conf.d/99-sail.ini
RUN chmod +x /usr/local/bin/start-container
EXPOSE 80/tcp
+52
View File
@@ -0,0 +1,52 @@
user sail;
worker_processes auto;
pid /run/nginx.pid;
error_log /dev/stderr warn;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
client_max_body_size 100m;
server {
listen 80 default_server;
server_name _;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /healthz {
allow 127.0.0.1;
deny all;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /dev/null;
fastcgi_pass 127.0.0.1:9000;
}
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_pass 127.0.0.1:9000;
fastcgi_read_timeout 60s;
}
location ~ /\. {
deny all;
}
}
}
+25
View File
@@ -0,0 +1,25 @@
[www]
user = sail
group = sail
listen = 127.0.0.1:9000
listen.owner = sail
listen.group = sail
pm = dynamic
pm.max_children = 8
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 4
pm.max_requests = 500
request_terminate_timeout = 60s
request_slowlog_timeout = 10s
slowlog = /proc/self/fd/2
ping.path = /healthz
ping.response = pong
clear_env = no
catch_workers_output = yes
decorate_workers_output = no
+23 -4
View File
@@ -4,10 +4,29 @@ user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:php]
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
user=%(ENV_SUPERVISOR_PHP_USER)s
environment=LARAVEL_SAIL="1"
[program:php-fpm]
command=/usr/sbin/php-fpm8.3 --nodaemonize --fpm-config /etc/php/8.3/fpm/php-fpm.conf
user=root
priority=10
autostart=true
autorestart=true
startsecs=2
stopasgroup=true
stopsignal=QUIT
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
user=root
priority=20
autostart=true
autorestart=true
startsecs=2
stopasgroup=true
stopsignal=QUIT
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
PROD_COMPOSE="${ROOT_DIR}/compose.prod.yaml"
DEV_COMPOSE="${ROOT_DIR}/compose.yaml"
ENV_FILE="${ROOT_DIR}/.env"
LEGACY_WAS_RUNNING=0
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
fail() {
printf '[error] %s\n' "$*" >&2
exit 1
}
env_value() {
local key="$1"
grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2- | sed -e 's/^"//' -e 's/"$//'
}
prod() {
docker compose -f "$PROD_COMPOSE" "$@"
}
legacy() {
docker compose -f "$DEV_COMPOSE" "$@"
}
wait_for_health() {
local service="$1"
local container_id
local status
container_id="$(prod ps -q "$service")"
[[ -n "$container_id" ]] || fail "Контейнер ${service} не создан"
for _ in $(seq 1 45); do
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id")"
if [[ "$status" == healthy ]]; then
return 0
fi
if [[ "$status" == exited || "$status" == dead ]]; then
docker logs --tail 100 "$container_id" >&2 || true
fail "Контейнер ${service} завершился со статусом ${status}"
fi
sleep 2
done
docker logs --tail 100 "$container_id" >&2 || true
fail "Контейнер ${service} не перешёл в healthy"
}
rollback_legacy() {
log "Production web не прошёл проверку, выполняю откат"
prod stop web app >/dev/null 2>&1 || true
if (( LEGACY_WAS_RUNNING )); then
legacy start laravel.test >/dev/null 2>&1 || true
fi
}
main() {
command -v docker >/dev/null 2>&1 || fail "Не найден Docker"
docker compose version >/dev/null 2>&1 || fail "Не найден Docker Compose Plugin"
[[ -f "$ENV_FILE" ]] || fail "Не найден ${ENV_FILE}"
cd "$ROOT_DIR"
local app_uid app_gid app_port legacy_id
app_uid="$(env_value WWWUSER)"
app_gid="$(env_value WWWGROUP)"
app_port="$(env_value APP_PORT)"
app_uid="${app_uid:-1000}"
app_gid="${app_gid:-1000}"
app_port="${app_port:-8080}"
mkdir -p storage/app/public storage/framework/cache/data storage/framework/sessions storage/framework/views storage/logs
if [[ "$(id -u)" -eq 0 ]]; then
chown -R "${app_uid}:${app_gid}" storage
fi
chmod -R ug+rwX storage
log "Собираю production-образы"
prod build app web
log "Запускаю новый PHP-FPM"
prod up -d --no-deps app
wait_for_health app
log "Выполняю миграции и прогреваю кэш"
prod exec -T --user "${app_uid}:${app_gid}" app php artisan migrate --force
prod exec -T --user "${app_uid}:${app_gid}" app php artisan optimize
legacy_id="$(legacy ps -q laravel.test 2>/dev/null || true)"
if [[ -n "$legacy_id" ]] && [[ "$(docker inspect -f '{{.State.Running}}' "$legacy_id")" == true ]]; then
LEGACY_WAS_RUNNING=1
log "Останавливаю legacy Sail-контейнер"
legacy stop laravel.test
fi
log "Переключаю nginx на production web-контейнер"
if ! prod up -d --no-deps web || ! wait_for_health web; then
rollback_legacy
exit 1
fi
if ! curl --fail --silent --max-time 15 "http://127.0.0.1:${app_port}/" >/dev/null; then
rollback_legacy
fail "HTTP-проверка production web завершилась ошибкой"
fi
log "Production-деплой завершён: app и web healthy"
prod ps app web
}
main "$@"