commit 7d96bc9b05e835945a09ee532192f7ee6ca23996 Author: Петрищев Алексей Date: Thu Nov 13 12:22:20 2025 +0300 Initial commit diff --git a/.env b/.env new file mode 100644 index 0000000..23859ff --- /dev/null +++ b/.env @@ -0,0 +1,17 @@ +# .env + +APP_ENV=dev +# APP_ENV=prod + + +# MySQL root +MYSQL_ROOT_PASSWORD=0ntbbW5M3Mcx6tivDGJJ + +# Основная база данных +MYSQL_DATABASE=app_db +MYSQL_USER=app_user +MYSQL_PASSWORD=app_pass + +# Для certbot / nginx +DOMAIN=zootovar-spb.ru +EMAIL=info@zootovar-spb.ru \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3689c93 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# Bambolo App — Dockerized Development & Production Environment + +## 📦 Структура контейнеров (Docker Compose) + +| Сервис | Назначение | +|--------------------|----------------------------------------------------------------------------| +| `php` | Основной PHP-FPM backend, обрабатывает web-запросы | +| `php-cron` | Контейнер для выполнения cron задач (в dev-среде — неактивен) | +| `bambolo-worker` | Gearman-воркер для фоновых задач | +| `nginx` | Веб-сервер с SSL (Let's Encrypt или self-signed) | +| `mariadb` | База данных MariaDB 10.6 | +| `memcached` | Кэш-сервер | +| `gearman` | Сервер очередей задач Gearman | +| `manticore_search` | Поисковый движок Manticore (основные индексы) | +| `manticore_logs` | Manticore для realtime-логов | +| `certbot` | Автоматическое продление SSL-сертификатов от Let's Encrypt | + +--- + +## 🗂 Структура проекта + +```text +. +├── app/ # Код проекта (монтируется в контейнеры) +│ └── www/ # Публичная директория +│ └── index.php +├── config/ +│ ├── php/ # Конфигурация PHP (php.ini, extensions) +│ ├── cron/ # Файлы cron для php-cron контейнера +│ ├── nginx/ # nginx конфиги по средам +│ ├── certs/ # SSL сертификаты Let's Encrypt +│ └── manticore_search/ # Конфиг поиска +│ └── manticore_logs/ # Конфиг поиска для realtime +├── db/ +│ ├── mysql/ # Том для MariaDB +│ ├── init/ # SQL-инициализация базы +│ ├── manticore_search/ # Данные поиска +│ └── manticore_logs/ +├── logs/ # Логи всех сервисов +├── sessions/ # Сессии PHP +├── docker/ # Dockerfile'ы и entrypoint-скрипты +│ ├── dockerfile_php +│ ├── dockerfile_cron +│ ├── dockerfile_worker +│ └── entrypoints/ +│ └── ... +├── .env # Переменные окружения +├── docker-compose.yml +└── README.md +``` + +--- + +## 🚀 Как пользоваться + +### 1. Сборка контейнеров + + +```bash +docker-compose build php-base +docker-compose build +``` + +### 2. Запуск окружения + +```bash +docker-compose up -d +``` + +При первом запуске нужно не забыть сделать +```bash +composer update +``` + +### 3. Переменные окружения (`.env`) + +```ini +APP_ENV=dev # или prod +MYSQL_ROOT_PASSWORD=root +MYSQL_DATABASE=bambolo +MYSQL_USER=bambolo +MYSQL_PASSWORD=secret +``` + +### 4. Cron и воркеры + +- В `APP_ENV=dev`: + - `php-cron` спит (`tail -f /dev/null`) + - `bambolo-worker` можно тоже "усыпить" при необходимости +- В `prod`: запускается cron + воркеры через `command:` с проверкой переменной окружения + +--- + +## 🧪 Полезные команды + +```bash +docker-compose exec php bash # доступ внутрь PHP +docker-compose exec mariadb mysql -u... # консоль базы +docker-compose logs -f manticore_search # логи поиска +docker-compose run --rm php-cron sh # отладка cron +``` + +--- + +## 🛡 Защита от случайных обновлений + +Все образы используют зафиксированные версии (`nginx:1.24.0`, `mariadb:10.6.16`, и т.д.), чтобы избежать неожиданных изменений при `docker pull`. + +--- + +## 🤝 Авторы + +Bambolo Dev Team, 2025 \ No newline at end of file diff --git a/config/certs/selfsigned/cert.pem b/config/certs/selfsigned/cert.pem new file mode 100644 index 0000000..899aeae --- /dev/null +++ b/config/certs/selfsigned/cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUFLAUw+h50C1UfSrbmuwG0c7ie7IwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDQxNjA4NTg0NFoXDTI2MDQx +NjA4NTg0NFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAs3ks5jJYQRb3Do9JtIBL84X6b94BJ3AkxhtQTuEmzFO1 ++OZgsyT2eVrbpfQq8MKAQG2c4x9Np3pbX5tA5ee/mWIKGorbG4KdvXTErXqZyVyA +zddsQ6B3Rp7yZuXhRuTcp5pt5dYwg/9YANIm8ftjPtSJe1DXO65dznu7QKzdOIyl +S0Xjs3KxgmaMHIDkTlEXTBoxwSU9eU3tjlQXWIv8nN/ab+py2msTcsJdfrO29CjN +cTm482yVaelHADgAh1mP3g92BDCR8mr11nZZNecFb2VZhGnz2Ipd03B99ELPumeD +o/TL8zW8Taxan/Hy5OFETkl5Fe1PfQ9kI4W0RzacEQIDAQABo1MwUTAdBgNVHQ4E +FgQUvmj+Ljw/DGDzWUacdIFmjWbW/+4wHwYDVR0jBBgwFoAUvmj+Ljw/DGDzWUac +dIFmjWbW/+4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAoqnC +457WLvpgFcDsQCjujz3Qv7EeK24csJfdMGBqs/1qkulF4jRwXzwe6sRyKsvuVYOW +WwtM8EgtBA9mVOL6mpudEZP0/WKeDDNxuspmc5uXtYeOZOu/1HgNSVFhY5eQW09E +OGsyY32K1wMoZT5qrRXjnOB1Us+U5G0D98KSW+Xs3m6p+FgTantQZ1D2xKQhzVbR +q47a9rLhdl7rb70HXKD6h0vyc1iVeF6HMSNDuGDT5lVHig3+oS6CwI/ovqE7Etbq +Mu6/uU7QOE5Zp33u9gRk11rzWyBm5NthjakssyArz9k8be5XCeOBTYjNEZopNRVW +V9K6Hckw4vKhMXdgZQ== +-----END CERTIFICATE----- diff --git a/config/certs/selfsigned/key.pem b/config/certs/selfsigned/key.pem new file mode 100644 index 0000000..ebc7361 --- /dev/null +++ b/config/certs/selfsigned/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzeSzmMlhBFvcO +j0m0gEvzhfpv3gEncCTGG1BO4SbMU7X45mCzJPZ5Wtul9CrwwoBAbZzjH02neltf +m0Dl57+ZYgoaitsbgp29dMStepnJXIDN12xDoHdGnvJm5eFG5Nynmm3l1jCD/1gA +0ibx+2M+1Il7UNc7rl3Oe7tArN04jKVLReOzcrGCZowcgOROURdMGjHBJT15Te2O +VBdYi/yc39pv6nLaaxNywl1+s7b0KM1xObjzbJVp6UcAOACHWY/eD3YEMJHyavXW +dlk15wVvZVmEafPYil3TcH30Qs+6Z4Oj9MvzNbxNrFqf8fLk4UROSXkV7U99D2Qj +hbRHNpwRAgMBAAECggEALZGW1NvzjJNIJ2o6TJpGro5WCBFWOa0/qQ1GVsDUGQ0o +JwpXgyTfb/Ch1IEqZfb8mV87dm98SAnJKX1B/R3aiBdceWDIQTPo2THMvj9izL2v +9pO76W/U2RmCcp3rlRSG8gdD4BeCOgGbVpoFSWuKx2kvfyAHhh+/sa243bMGeFTQ +PJZH+bMkh5tNllRGGpQ/i1+XflQWjLp4wgeg98eVIIQf4/hdkSVCMkY+Cf4Vtr8C +cxrwIon2vt12MEmuKhKo6j07Evj1Aj6ppQJrU0fJNeDforNtzfolPQWM50sbwSt4 ++9VKHnykKdLD9x7TMMRL+kBYVHnnDXgEmbADM3IkAQKBgQDtD3B5ccVrY1ZFnwvF +fEBSdJmU1MAcOibZK5kX9FQ3r0eC0BQRNID5947zP6TGoUJ/xn45b49bZ5JaYCot +L6fvBfyMbjr7YlFqmXAwXu7ZeDMT/wxIxRWm66bhYQ1fKG4o+USOpqp1+YrN32q6 +Neqr/4OznJJrZaLnSPlOiwh/DwKBgQDBz+sBpDanrdXbY1Yo7jbEWvY6/HaIIF4i +OV7j1NMj0aMqsygUs6fVLkVhIpBrkpSvW4LVNyerGqTgmSnpMUf7efLUqNnlZ+uT +jRWpfZFVWvrNV8fAt0WmTYd9XK98MlrryP3ut8NbZf5iQvJiyDOw8gd0ak4o5rMa +Hql9zhEy3wKBgHUxj8oKC64WMt1CTmB4F5hr2k0wjDSoLvJn0kx8VnIc7f5mfbUv +vp9U2k+44+3qZOSkLVyZoUZvwnN5XQBvsdbBn+OQzwndxiAr8MGI/Q13ldDJ4rnK +7PRTRXHgN+sWIreQ22qmTFj8X7l9PNcHtpcHP4W43s3HNiye79j7dNzTAoGBALws +8qDqXsKZTq2vGkWtXHFzW+VToIZ03tDd4RrWVZZOgd0Ai+bltAuQ3H3+QDih3kkQ +UcxQu+wud202aPHoDlrFQZdmxgEt0BW4AxNIPqagKijblK+xgieA2Q9HwX6VqZ+K +y7pOo5gHRGEFXS+58C5aIBDQ8khWDglLQgdK33oDAoGAA8h7C43Otljgjpt3gV3w +tGJFkkEv2RXhHA69dozV5tuI6sGRlumYkPYGAyDSjoQMWJ+3TutYykqTdLBnvlVe +gUMLOLWmSuw8/Sp5rC142Xx0Z+M5vJacGwQYhHLFyqPaoh3O5npKtVPv1J0WViv9 +Pzsd+llGQp/QURBlTRxHtDo= +-----END PRIVATE KEY----- diff --git a/config/cron/app.cron b/config/cron/app.cron new file mode 100644 index 0000000..fdffa2a --- /dev/null +++ b/config/cron/app.cron @@ -0,0 +1 @@ +# placeholder diff --git a/config/manticore_logs/manticore.conf.sh b/config/manticore_logs/manticore.conf.sh new file mode 100644 index 0000000..3430659 --- /dev/null +++ b/config/manticore_logs/manticore.conf.sh @@ -0,0 +1,9 @@ +searchd { + listen = 0.0.0.0:9312 + listen = 0.0.0.0:9306:mysql + + log = /var/log/manticore/searchd_logs.log + query_log = /var/log/manticore/query_logs.log + pid_file = /var/run/manticore/searchd_logs.pid + data_dir = /var/manticore +} diff --git a/config/manticore_search/manticore.conf.sh b/config/manticore_search/manticore.conf.sh new file mode 100644 index 0000000..95819d1 --- /dev/null +++ b/config/manticore_search/manticore.conf.sh @@ -0,0 +1,3 @@ +#!/bin/sh +envsubst < /etc/manticoresearch/manticore.template > /etc/manticoresearch/manticore.conf +envsubst < /etc/manticoresearch/manticore.template \ No newline at end of file diff --git a/config/manticore_search/manticore.template b/config/manticore_search/manticore.template new file mode 100644 index 0000000..b0348e6 --- /dev/null +++ b/config/manticore_search/manticore.template @@ -0,0 +1,142 @@ +##### +# Pitsbikes +########################### + +source pitbike +{ + type = mysql + sql_host = ${DB_HOST} + sql_user = ${DB_USERNAME} + sql_pass = ${DB_PASSWORD} + sql_db = ${DB_DATABASE} + sql_port = 3306 # optional, default is 3306 + sql_query_pre = SET NAMES utf8 + sql_query_pre = SET SESSION query_cache_type=OFF + + sql_query = SELECT `shop_item`.id, create_time, `shop_item`.`title` , description, page_keywords, mini_desc, page_title, page_h1, rating, price, ( SELECT group_concat( `shop_item_chars_vals`.`val` SEPARATOR ', ' ) FROM `shop_item_chars` LEFT JOIN `shop_item_chars_vals` ON (`shop_item_chars_vals`.`id` = `shop_item_chars`.`attr_value` ) WHERE `shop_item_chars`.`item_id` = `shop_item`.`id` ) AS tags,( SELECT group_concat( concat_ws(', ', `title`,`page_keywords`,`page_title`,`page_h1`) SEPARATOR ', ' ) FROM `shop_catalog` WHERE `shop_catalog`.`template_id` = `shop_item`.`template_id` ) AS catalog, `sizes`, `not_in_face`,`new` FROM shop_item where !`_sys_deleted` AND !`_sys_unvisible` + + sql_attr_bool = not_in_face + sql_attr_bool = new + sql_attr_uint = rating + sql_attr_uint = price + + sql_attr_multi = uint tag_id from query; SELECT `item_id` AS `doc_id` , `attr_value` AS `filter_id` FROM `shop_item_chars` + sql_attr_multi = uint size_id from query; SELECT `shop_item`.`id` AS `doc_id` , `shop_size`.`id` AS `filter_id` FROM `shop_size` RIGHT JOIN `shop_item` ON ( find_in_set( `shop_size`.`id` , `shop_item`.`sizes` ) ) + sql_attr_multi = uint size_all_id from query; SELECT `shop_item`.`id` AS `doc_id` , `shop_size`.`id` AS `filter_id` FROM `shop_size` RIGHT JOIN `shop_item` ON ( find_in_set( `shop_size`.`id` , `shop_item`.`sizes_all` ) ) + sql_attr_multi = uint place_id from query; SELECT `item_id` AS `doc_id` , `place` AS `filter_id` FROM `shop_goods` WHERE `status` = 2 AND `order_id` = 0 GROUP BY `item_id`, `place` + sql_attr_multi = uint catalog_id from query; SELECT `item_id` AS `doc_id` , `catalog_id` AS `filter_id` FROM `shop_catalog_items` + +} + +source pitbike_adm +{ + type = mysql + sql_host = ${DB_HOST} + sql_user = ${DB_USERNAME} + sql_pass = ${DB_PASSWORD} + sql_db = ${DB_DATABASE} + sql_port = 3306 # optional, default is 3306 + sql_query_pre = SET NAMES utf8 + sql_query_pre = SET SESSION query_cache_type=OFF + sql_query = SELECT `shop_item`.id,`shop_item`.`_sys_unvisible`, create_time, `shop_item`.`title`, `mainpic_preview` , description, page_keywords, mini_desc, page_title, page_h1, rating, price, ( SELECT group_concat( `shop_item_chars_vals`.`val` SEPARATOR ', ' ) FROM `shop_item_chars` LEFT JOIN `shop_item_chars_vals` ON (`shop_item_chars_vals`.`id` = `shop_item_chars`.`attr_value` ) WHERE `shop_item_chars`.`item_id` = `shop_item`.`id` ) AS tags,( SELECT group_concat( concat_ws(', ', `title`,`page_keywords`,`page_title`,`page_h1`) SEPARATOR ', ' ) FROM `shop_catalog` WHERE `shop_catalog`.`template_id` = `shop_item`.`template_id` ) AS catalog, `sizes`, `not_in_face`,`new` FROM shop_item where !`_sys_deleted` + + sql_attr_bool = not_in_face + sql_attr_bool = new + sql_attr_bool = _sys_unvisible + sql_attr_uint = rating + sql_attr_uint = price + sql_attr_uint = mainpic_preview + + sql_attr_multi = uint tag_id from query; SELECT `item_id` AS `doc_id` , `attr_value` AS `filter_id` FROM `shop_item_chars` + sql_attr_multi = uint size_id from query; SELECT `shop_item`.`id` AS `doc_id` , `shop_size`.`id` AS `filter_id` FROM `shop_size` RIGHT JOIN `shop_item` ON ( find_in_set( `shop_size`.`id` , `shop_item`.`sizes` ) ) + sql_attr_multi = uint size_all_id from query; SELECT `shop_item`.`id` AS `doc_id` , `shop_size`.`id` AS `filter_id` FROM `shop_size` RIGHT JOIN `shop_item` ON ( find_in_set( `shop_size`.`id` , `shop_item`.`sizes_all` ) ) + sql_attr_multi = uint place_id from query; SELECT `item_id` AS `doc_id` , `place` AS `filter_id` FROM `shop_goods` WHERE `status` = 2 AND `order_id` = 0 GROUP BY `item_id`, `place` + sql_attr_multi = uint catalog_id from query; SELECT `item_id` AS `doc_id` , `catalog_id` AS `filter_id` FROM `shop_catalog_items` + +} + +source pitbike_cats +{ + type = mysql + sql_host = ${DB_HOST} + sql_user = ${DB_USERNAME} + sql_pass = ${DB_PASSWORD} + sql_db = ${DB_DATABASE} + sql_port = 3306 # optional, default is 3306 + sql_query_pre = SET NAMES utf8 + sql_query_pre = SET SESSION query_cache_type=OFF + sql_query = SELECT id, title, page_keywords, page_h1, page_title FROM `shop_catalog` WHERE `_sys_unvisible` = 0 AND `_sys_deleted` = 0 +} + +index pitbike +{ + source = pitbike + path = /usr/share/manticore/pitbike + docinfo = extern + mlock = 0 + morphology = stem_enru, soundex, metaphone + min_word_len = 2 + min_infix_len = 3 + index_exact_words = 1 + html_strip = 1 +} + +index pitbike_adm +{ + source = pitbike_adm + path = /usr/share/manticore/pitbike_adm + docinfo = extern + mlock = 0 + morphology = stem_enru, soundex, metaphone + min_word_len = 2 + min_infix_len = 3 + index_exact_words = 1 + html_strip = 1 +} + +index pitbike_cats +{ + source = pitbike_cats + path = /usr/share/manticore/pitbike_cats + docinfo = extern + mlock = 0 + morphology = stem_enru, soundex, metaphone + index_exact_words = 1 + min_word_len = 2 + min_infix_len = 3 + html_strip = 1 +} + + +############################################################################# +## indexer settings +############################################################################# + +indexer +{ + mem_limit = 64M +} + +############################################################################# +## searchd settings +############################################################################# + +searchd +{ + #listen = 0.0.0.0:9312 + #listen = 127.0.0.1:3307:mysql41 + listen = 0.0.0.0:9312 + listen = 0.0.0.0:9306:mysql + listen = 0.0.0.0:9308:http + + pseudo_sharding = 0 + log = /var/log/manticore/searchd.log + query_log = /var/log/manticore/query.log + read_timeout = 5 + client_timeout = 300 + max_children = 30 + pid_file = /var/run/manticore/searchd.pid + seamless_rotate = 1 + preopen_indexes = 1 + unlink_old = 1 +} diff --git a/config/nginx/default.conf.dev b/config/nginx/default.conf.dev new file mode 100644 index 0000000..2369086 --- /dev/null +++ b/config/nginx/default.conf.dev @@ -0,0 +1,96 @@ +server { + listen 80; + server_name localhost .localhost; + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + server_name localhost .localhost; + + root /var/www/html/www; + index index.php; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + ssl_certificate /etc/ssl/selfsigned/cert.pem; + ssl_certificate_key /etc/ssl/selfsigned/key.pem; + + client_max_body_size 128M; + + location / { + rewrite ^/(.*)/$ /$1 permanent; + + if ($scheme != 'https') { + return 301 https://$host$request_uri; + } + + if ($host != "localhost") { + return 301 https://localhost$request_uri; + } + + rewrite ^/index.php$ / permanent; + + try_files $uri $uri/ /index.php?q=$uri&$args; + } + + location /admin { + try_files $uri $uri/ /admin/index.php?q=$uri&$args; + } + + location ~ \.php$ { + include fastcgi_params; + fastcgi_pass php:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME /var/www/html/www$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_read_timeout 180; + proxy_buffers 8 32k; + proxy_buffer_size 64k; + } + + location ~* \.(jpg|jpeg|gif|webp|png|css|zip|tgz|gz|rar|bz2|doc|xls|pdf|tar|wav|bmp|swf|ico|txt|js|htc|flv|psd|cdr|woff|ttf|svg|eot|woff2)$ { + root /var/www/html/www; + add_header Access-Control-Allow-Origin *; + try_files $uri $uri/ @static; + access_log off; + expires 30d; + } + + location @static { + rewrite "^/images/(\d{2})/(\d{2})/(\d{1,}).jpg$" /img.php?id=$3 last; + rewrite "^/images/([0-9]{2})([0-9]{2})([0-9]{1,}).webp$" /images/$1/$2/$1$2$3.webp permanent; + rewrite "^/video/(\d{1,}).flv$" /video.php?id=$1 last; + rewrite "^/avatars/(\d{1,}).jpg$" /avatars.php?id=$1 last; + rewrite "^/avatars/(\d{1,}-\w+).jpg$" /avatars.php?id=$1 last; + } + + location = /favicon.ico { + log_not_found off; + access_log off; + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + location ~ /\.ht { + deny all; + } + + location = /status { + allow 127.0.0.1; + deny all; + access_log off; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /var/www/html/www/index.php; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_pass php:9000; + } +} diff --git a/config/nginx/default.conf.prod b/config/nginx/default.conf.prod new file mode 100644 index 0000000..657e51c --- /dev/null +++ b/config/nginx/default.conf.prod @@ -0,0 +1,102 @@ +server { + listen 80; + server_name bambolo.ru .bambolo.ru; + + location /.well-known/acme-challenge/ { + root /var/www/html/www; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + server_name bambolo.ru .bambolo.ru; + + root /var/www/html/www; + index index.php; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + ssl_certificate /etc/letsencrypt/live/bambolo.ru/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/bambolo.ru/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + client_max_body_size 128M; + + location / { + rewrite ^/(.*)/$ /$1 permanent; + + if ($scheme != 'https') { + return 301 https://$host$request_uri; + } + + if ($host != "bambolo.ru") { + return 301 https://bambolo.ru$request_uri; + } + + rewrite ^/index.php$ / permanent; + + try_files $uri $uri/ /index.php?q=$uri&$args; + } + + location /admin { + try_files $uri $uri/ /admin/index.php?q=$uri&$args; + } + + location ~ \.php$ { + include fastcgi_params; + fastcgi_pass php:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME /var/www/html/www$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_read_timeout 180; + proxy_buffers 8 32k; + proxy_buffer_size 64k; + } + + location ~* \.(jpg|jpeg|gif|webp|png|css|zip|tgz|gz|rar|bz2|doc|xls|pdf|tar|wav|bmp|swf|ico|txt|js|htc|flv|psd|cdr|woff|ttf|svg|eot|woff2)$ { + root /var/www/html/www; + add_header Access-Control-Allow-Origin *; + try_files $uri $uri/ @static; + access_log off; + expires 30d; + } + + location @static { + rewrite "^/images/(\d{2})/(\d{2})/(\d{1,}).jpg$" /img.php?id=$3 last; + rewrite "^/images/([0-9]{2})([0-9]{2})([0-9]{1,}).webp$" /images/$1/$2/$1$2$3.webp permanent; + rewrite "^/video/(\d{1,}).flv$" /video.php?id=$1 last; + rewrite "^/avatars/(\d{1,}).jpg$" /avatars.php?id=$1 last; + rewrite "^/avatars/(\d{1,}-\w+).jpg$" /avatars.php?id=$1 last; + } + + location = /favicon.ico { + log_not_found off; + access_log off; + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + location ~ /\.ht { + deny all; + } + + location = /status { + allow 127.0.0.1; + deny all; + access_log off; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /var/www/html/www/index.php; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_pass php:9000; + } +} diff --git a/config/php/php.ini b/config/php/php.ini new file mode 100644 index 0000000..e4f974d --- /dev/null +++ b/config/php/php.ini @@ -0,0 +1,59 @@ +[PHP] +; Включение установленных модулей +extension=bcmath +extension=calendar +extension=exif +extension=gettext +extension=gd +extension=intl +extension=mbstring +extension=mysqli +extension=opcache +extension=pcntl +extension=pdo +extension=pdo_mysql +extension=posix +extension=shmop +extension=snmp +extension=soap +extension=sockets +extension=sysvmsg +extension=sysvsem +extension=sysvshm +extension=xsl +extension=zip +extension=memcached +extension=gearman +extension=imagick +extension=igbinary +extension=msgpack +extension=geoip +extension=fann +extension=sodium + + +; Остальные параметры +display_errors = Off +log_errors = On +memory_limit = 1024M +max_execution_time = 30 +post_max_size = 100M +upload_max_filesize = 200M +max_input_vars = 10000 +session.save_handler = files +session.save_path = "/var/lib/php/sessions" +session.gc_probability = 1 +session.gc_divisor = 1000 +session.gc_maxlifetime = 1440 +session.sid_length = 26 +session.sid_bits_per_character = 6 +short_open_tag = On +output_buffering = 4096 +pdo_mysql.cache_size = 2000 +mysqli.allow_persistent = On +mysqli.cache_size = 2000 +mysqli.max_persistent = -1 +mysqli.max_links = -1 +opcache.enable = 1 +error_log = /var/log/php-error.log +date.timezone = Europe/Moscow \ No newline at end of file diff --git a/config/php/www.conf b/config/php/www.conf new file mode 100644 index 0000000..9a81c96 --- /dev/null +++ b/config/php/www.conf @@ -0,0 +1,22 @@ +[www] +user = www-data +group = www-data + +listen = 9000 +listen.owner = www-data +listen.group = www-data + +pm = dynamic +pm.max_children = 70 +pm.start_servers = 30 +pm.min_spare_servers = 30 +pm.max_spare_servers = 50 + +request_terminate_timeout = 30 + +chdir = / +catch_workers_output = yes + +php_flag[display_errors] = off +php_admin_value[error_log] = /var/log/fpm-php.www.log +php_admin_flag[log_errors] = on diff --git a/deploy.cmd b/deploy.cmd new file mode 100644 index 0000000..f8f73f4 --- /dev/null +++ b/deploy.cmd @@ -0,0 +1,58 @@ +@echo off +chcp 65001 >nul +setlocal + +set ACTION=%1 +if "%ACTION%"=="" set ACTION=help + +if "%ACTION%"=="help" ( + echo. + echo [DEPLOY] Справка по командам: + echo --------------------------------------------- + echo base-build — Сборка базового php-образа + echo build — Сборка всех контейнеров + echo up — Запуск контейнеров + echo down — Остановка и удаление контейнеров + echo restart — Перезапуск всех контейнеров + echo logs — Просмотр логов всех сервисов + echo php-log — Логи только php-fpm + echo cron-log — Логи только cron-контейнера + echo worker-log — Логи фонового воркера + echo help — Показать эту справку + echo --------------------------------------------- + goto :eof +) + +echo [INFO] Старт скрипта deploy.cmd +echo [DEBUG] ACTION=%ACTION% + +if "%ACTION%"=="base-build" ( + echo [INFO] Сборка базового образа bambolo/php-base:latest + docker build -f docker/dockerfile_base -t bambolo/php-base:latest . +) else if "%ACTION%"=="build" ( + echo [INFO] Сборка всех контейнеров... + docker-compose build +) else if "%ACTION%"=="up" ( + echo [INFO] Запуск контейнеров... + docker-compose up -d +) else if "%ACTION%"=="down" ( + echo [INFO] Остановка и удаление контейнеров... + docker-compose down +) else if "%ACTION%"=="restart" ( + echo [INFO] Перезапуск контейнеров... + docker-compose down + docker-compose up -d +) else if "%ACTION%"=="logs" ( + docker-compose logs -f +) else if "%ACTION%"=="php-log" ( + docker-compose logs -f php +) else if "%ACTION%"=="cron-log" ( + docker-compose logs -f php-cron +) else if "%ACTION%"=="worker-log" ( + docker-compose logs -f bambolo-worker +) else ( + echo [ERROR] Неизвестная команда: %ACTION% + echo Для справки используйте: deploy.cmd help +) + +echo [DONE] diff --git a/docker-build-base.cmd b/docker-build-base.cmd new file mode 100644 index 0000000..8a3b9c6 --- /dev/null +++ b/docker-build-base.cmd @@ -0,0 +1 @@ +docker-compose -f docker-compose.build.yml build diff --git a/docker-compose.build.yml b/docker-compose.build.yml new file mode 100644 index 0000000..fff190c --- /dev/null +++ b/docker-compose.build.yml @@ -0,0 +1,7 @@ +services: + + php-base: + build: + context: . + dockerfile: docker/dockerfile_base + image: angels-it/php-base diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..39bdf9c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,174 @@ +services: + + php: + build: + context: . + dockerfile: docker/dockerfile_php + environment: + DB_HOST: mariadb + DB_DATABASE: ${MYSQL_DATABASE} + DB_USERNAME: ${MYSQL_USER} + DB_PASSWORD: ${MYSQL_PASSWORD} + image: angels-it/php-app + volumes: + - ./app:/var/www/html + - ./sessions:/var/lib/php/sessions + - ./config/php:/usr/local/etc/php + - ./logs/php:/var/log + networks: + - app_network + + php-cron: + build: + context: . + dockerfile: docker/dockerfile_cron + image: angels-it/php-cron + environment: + APP_ENV: ${APP_ENV} + DB_HOST: mariadb + DB_DATABASE: ${MYSQL_DATABASE} + DB_USERNAME: ${MYSQL_USER} + DB_PASSWORD: ${MYSQL_PASSWORD} + volumes: + - ./app:/var/www/html + - ./config/php:/usr/local/etc/php + - ./config/cron:/etc/cron.d + - ./logs/php-cron:/var/log + command: > + /bin/sh -c ' + if [ "$${APP_ENV}" = "prod" ]; then + echo "[cron] production mode — starting cron"; + cron -f; + else + echo "[cron] dev mode — sleeping"; + tail -f /dev/null; + fi + ' + restart: always + networks: + - app_network + + +# bambolo-worker: +# build: +# context: . +# dockerfile: docker/dockerfile_worker +# image: angels-it/worker +# environment: +# DB_HOST: mariadb +# DB_DATABASE: ${MYSQL_DATABASE} +# DB_USERNAME: ${MYSQL_USER} +# DB_PASSWORD: ${MYSQL_PASSWORD} +# volumes: +# - ./app:/var/www/html +# - ./config/php:/usr/local/etc/php +# - ./logs/worker:/var/log +# restart: always +# networks: +# - app_network + + nginx: + image: nginx:1.24.0 + ports: + - "80:80" + - "443:443" + depends_on: + - php + volumes: + - ./app:/var/www/html + - ./config/nginx/default.conf.${APP_ENV}:/etc/nginx/conf.d/default.conf + - ./config/certs:/etc/letsencrypt + - ./config/certs-data:/data/letsencrypt + - ./config/certs/selfsigned:/etc/ssl/selfsigned + - ./logs/nginx:/var/log/nginx + networks: + - app_network + + mariadb: + image: mariadb:10.6 + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + volumes: + - ./db/mysql:/var/lib/mysql + - ./db/init:/docker-entrypoint-initdb.d + + command: [ + '--default_authentication_plugin=mysql_native_password', + '--character-set-server=utf8mb4', + '--collation-server=utf8mb4_unicode_ci', + '--sql_mode=NO_ENGINE_SUBSTITUTION' + ] + networks: + - app_network + + memcached: + image: memcached:1.6.38 + networks: + - app_network + +# gearman: +# image: artefactual/gearmand:1.1.21.2-alpine +# restart: unless-stopped +# networks: +# - app_network + + manticore_search: + build: + context: . + dockerfile: docker/dockerfile_manticore_search + image: manticoresearch/manticore:6.3.8 + user: root + volumes: + - ./db/manticore_search:/var/manticore + - ./config/manticore_search:/etc/manticoresearch + - ./logs/manticore_search:/var/log/manticore + environment: + DB_HOST: mariadb + DB_DATABASE: ${MYSQL_DATABASE} + DB_USERNAME: ${MYSQL_USER} + DB_PASSWORD: ${MYSQL_PASSWORD} + networks: + - app_network + + manticore_logs: + image: manticoresearch/manticore:6.3.8 + volumes: + - ./db/manticore_logs:/var/manticore + - ./config/manticore_logs:/etc/manticoresearch + - ./logs/manticore_logs:/var/log/manticore + networks: + - app_network + + certbot: + image: certbot/certbot + volumes: + - ./config/certs:/etc/letsencrypt + - ./config/certs-data:/data/letsencrypt + - ./app:/var/www/html/ + - ./logs/certbot:/var/log/letsencrypt + entrypoint: /bin/sh -c + command: > + "trap exit TERM; + while :; do + certbot renew --webroot -w /var/www/html/www --quiet; + sleep 12h & wait $${!}; + done" + networks: + - app_network + + phpmyadmin: + image: phpmyadmin/phpmyadmin:latest + ports: + - "8080:80" + environment: + - PMA_HOST=mariadb + - PMA_PORT=3306 + networks: + - app_network + +networks: + app_network: + driver: bridge diff --git a/docker-down.cmd b/docker-down.cmd new file mode 100644 index 0000000..58694d0 --- /dev/null +++ b/docker-down.cmd @@ -0,0 +1 @@ +docker-compose down \ No newline at end of file diff --git a/docker-up.cmd b/docker-up.cmd new file mode 100644 index 0000000..5177d11 --- /dev/null +++ b/docker-up.cmd @@ -0,0 +1 @@ +docker-compose up -d \ No newline at end of file diff --git a/docker/dockerfile_base b/docker/dockerfile_base new file mode 100644 index 0000000..a676840 --- /dev/null +++ b/docker/dockerfile_base @@ -0,0 +1,67 @@ +FROM php:7.4-fpm + +# Установка системных зависимостей по частям для кэшируемости и отладки +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential autoconf gcc make + +RUN apt-get install -y --no-install-recommends \ + libfreetype6-dev libjpeg62-turbo-dev libpng-dev libwebp-dev libxpm-dev + +RUN apt-get install -y --no-install-recommends \ + libonig-dev libxml2-dev libzip-dev zlib1g-dev + +RUN apt-get install -y --no-install-recommends \ + libicu-dev libxslt1-dev + +RUN apt-get install -y --no-install-recommends \ + libmemcached-dev libmagickwand-dev libpspell-dev libsnmp-dev + +RUN apt-get install -y --no-install-recommends \ + libreadline-dev libfann-dev libgeoip-dev libgearman-dev + +RUN apt-get install -y --no-install-recommends \ + unzip zip cron supervisor + +# Настройка GD +RUN docker-php-ext-configure gd \ + --with-freetype \ + --with-jpeg \ + --with-webp \ + --with-xpm + + +# Установка PHP-расширений по частям +RUN docker-php-ext-install -j$(nproc) bcmath calendar exif gettext gd +RUN docker-php-ext-install -j$(nproc) intl mbstring mysqli opcache pcntl +RUN docker-php-ext-install -j$(nproc) pdo pdo_mysql posix shmop +RUN docker-php-ext-install -j$(nproc) snmp soap sockets sysvmsg sysvsem sysvshm +RUN docker-php-ext-install -j$(nproc) xsl zip + +RUN apt-get update && apt-get install -y libsodium-dev \ + && docker-php-ext-install sodium + + +# Очистка +RUN apt-get clean && rm -rf /var/lib/apt/lists/* + +# Установка PECL-расширений +RUN pecl install memcached-3.2.0 && docker-php-ext-enable memcached +RUN pecl install gearman-2.1.0 && docker-php-ext-enable gearman +RUN pecl install imagick-3.7.0 && docker-php-ext-enable imagick +RUN pecl install igbinary-3.2.14 && docker-php-ext-enable igbinary +RUN pecl install msgpack-2.1.2 && docker-php-ext-enable msgpack +RUN pecl install geoip-1.1.1 && docker-php-ext-enable geoip +RUN pecl install fann-1.1.1 && docker-php-ext-enable fann + +RUN ln -sf /usr/share/zoneinfo/Europe/Moscow /etc/localtime && echo "Europe/Moscow" > /etc/timezone + + +# Установка composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# Общий конфиг +COPY ./config/php/php.ini /usr/local/etc/php/ +COPY ./config/php/www.conf /usr/local/etc/php-fpm.d/www.conf + +# Рабочая директория +WORKDIR /var/www/html diff --git a/docker/dockerfile_cron b/docker/dockerfile_cron new file mode 100644 index 0000000..6801cea --- /dev/null +++ b/docker/dockerfile_cron @@ -0,0 +1,7 @@ +FROM angels-it/php-base:latest + +COPY ./config/cron /etc/cron.d +RUN chmod 0644 /etc/cron.d/* && crontab /etc/cron.d/* + +WORKDIR /var/www/html +CMD ["cron", "-f"] diff --git a/docker/dockerfile_manticore_search b/docker/dockerfile_manticore_search new file mode 100644 index 0000000..7a23042 --- /dev/null +++ b/docker/dockerfile_manticore_search @@ -0,0 +1,3 @@ +FROM manticoresearch/manticore:6.3.8 + +RUN apt-get update && apt-get install -y gettext-base \ No newline at end of file diff --git a/docker/dockerfile_php b/docker/dockerfile_php new file mode 100644 index 0000000..efa586f --- /dev/null +++ b/docker/dockerfile_php @@ -0,0 +1,4 @@ +FROM angels-it/php-base:latest + +WORKDIR /var/www/html +CMD ["php-fpm"] \ No newline at end of file diff --git a/docker/dockerfile_worker b/docker/dockerfile_worker new file mode 100644 index 0000000..fe559e9 --- /dev/null +++ b/docker/dockerfile_worker @@ -0,0 +1,4 @@ +FROM angels-it/php-base:latest + +WORKDIR /var/www/html/www/admin/workers/ +CMD ["php", "/var/www/html/www/admin/workers/_gearman_client.php"] \ No newline at end of file