Symfony-ის deploy ყოველთვის ერთსა და იმავე მოკლე სიას მისდევს: production რეჟიმი ჩართულია, სწორი გარემოს ცვლადები ადგილზეა, დამოკიდებულებები dev პაკეტების გარეშეა დაინსტალირებული, cache გახურებულია, ბაზა მიგრირებულია, var/ დირექტორიაში ჩაწერა შეიძლება და გადაწყვეტილია, რა გაუშვებს შენს Messenger worker-ებს. FrankenPHP ამ სიას არ ცვლის. ის ცვლის სერვერს ამ სიის ქვეშ: nginx-ისა და PHP-FPM-ის ნაცვლად ერთი პროცესი, რომელიც Caddy-ზეა აგებული, ემსახურება HTTP-ს და უშვებს PHP-ს. Symfony აპლიკაციისთვის ეს უმეტესად კარგი ამბავია, ეს გზამკვლევი კი იმ ადგილებს ეხება, სადაც ასე არ არის.
ის დაწერილია RE:NODE-ის Laravel / PHP ხაზისთვის, რომელიც Laravel-ს ან Symfony-ს უშვებს FrankenPHP-ზე PHP 8.3, 8.4 ან 8.5-ით, ხოლო Composer დამოკიდებულებებს სტარტზე აყენებს. ქვემოთ ყველა ბრძანების Symfony-ს ნაწილი ნებისმიერ ჰოსტზე ერთნაირია. პლატფორმის ნაწილი - სად ჯდება ცვლადები, როგორ აღწევს შენამდე proxy slot-ი, რას გაძლევს პანელი - ცალკე არის აღნიშნული, ხოლო რასაც პლატფორმა არ აღწერს, ვერ ვხვდებით და ამას პირდაპირ ვამბობთ.
რა იცვლება FrankenPHP-ით#
კლასიკურ PHP სტეკს სამი ნაწილი აქვს: ვებ სერვერი (nginx ან Apache), PHP-FPM და მათ შორის შუამავალი ლოგიკა. FrankenPHP ამას ერთ ბინარულ ფაილად აქცევს. ის PHP ინტერპრეტატორს Caddy-ზე აგებულ Go ვებ სერვერში ათავსებს, ამიტომ გვაქვს ერთი გასაშვები პროცესი, ერთი ლოგი წასაკითხად და არც fastcgi_pass ხაზი, რომელშიც შეცდომა დაუშვა.
Symfony აპლიკაციისთვის ამას სამი პრაქტიკული შედეგი აქვს.
- nginx-ის კონფიგურაციის გადატანა არ გჭირდება. Symfony-ის დოკუმენტაციის
try_filesდაfastcgi_split_path_infoბლოკები არსებობს იმისთვის, რომ ყველა მოთხოვნაpublic/index.php-ზე გადაიყვანოს. Caddy-ს აქვს front-controller-ის იდიომა, რომელიც იგივეს აკეთებს, ამიტომ FrankenPHP-ით მომსახურებულ Symfony პროექტს შენი ძველი server block არ სჭირდება. - Worker mode არსებობს და არჩევითია. ჩვეულებრივ რეჟიმში FrankenPHP PHP-FPM-ივით იქცევა: ყოველი მოთხოვნა ფრეიმვორქს ხელახლა ჩატვირთავს, მოთხოვნას დაამუშავებს და ყველაფერს გადააგდებს. worker mode-ში Symfony kernel ერთხელ იტვირთება და მოთხოვნებს შორის მეხსიერებაში რჩება, რაც მოთხოვნაზე დახარჯულ დროს მნიშვნელოვნად ამცირებს. ამისთვის გჭირდება
runtime/frankenphp-symfonyპაკეტი და ეხება იგივე გაფრთხილებები, რაც ნებისმიერ ხანგრძლივად მომუშავე PHP პროცესს: static state და სერვისების state მოთხოვნებს შორის გადადის, თუ სერვისები არ დარესეტდა. Symfony თავისი სერვისების უმეტესობას თვითონ არესეტებს, შენი კოდი კი შენი საქმეა. worker mode განიხილე როგორც დაზუსტების ნაბიჯი მას შემდეგ, რაც აპლიკაცია მუშაობს, და არა როგორც deploy-ის წინაპირობა. - PHP-ის ვერსია პლატფორმის პარამეტრია. RE:NODE ამ ხაზზე გთავაზობს 8.3-ს, 8.4-სა და 8.5-ს. Symfony 7.4-ს PHP 8.2 ან უფრო ახალი სჭირდება, Symfony 8.0-ს კი PHP 8.4, ამიტომ არჩევამდე შეამოწმე
require.phpშეზღუდვა დაsymfony/*ვერსიები შენსcomposer.json-ში. აიღე ყველაზე ახალი PHP, რომელსაც შენი მთელი დამოკიდებულებების ხე იძლევა.
ამ გზამკვლევში დანარჩენი ყველაფერი ჩვეულებრივი Symfony-ა. თუ ფიქრობ, რამდენი გადმოდის Laravel-დან, Laravel-ის production-ში გაშვება ამ სტეკის ფრეიმვორქის მხარეს მიმოიხილავს. ორი პოსტი ძირითადად სახელებით განსხვავდება: APP_ENV=prod და production, bin/console და artisan, Messenger და queue worker.
production რეჟიმი: APP_ENV, APP_DEBUG და APP_SECRET#
Symfony თითქმის ყველაფერს სამი ცვლადით წყვეტს. production-ში ისინი ასეთია:
APP_ENV=prodAPP_DEBUG=0APP_SECRET=replace-with-a-long-random-stringAPP_ENV=prod ირჩევს prod კონფიგურაციას და config/-ის when@prod ბლოკებს. ამასთან ფრეიმვორქი ყოველ მოთხოვნაზე აღარ აკომპილირებს container-ს და არ ამოწმებს თარგების ცვლილების დროს. APP_DEBUG=0 გამორთავს debug container-ს და გამონაკლისის გვერდს, რომელიც ფაილების გზებს, სერვისების არგუმენტებსა და გარემოს დეტალებს ბეჭდავს. APP_ENV=prod-ის APP_DEBUG=1-თან ერთად გაშვება შესაძლებელია, მაგრამ სჯობს ასე არ მოიქცე: production cache-ს მიიღებ და მასთან ერთად development-ის გაჟონვას.
APP_SECRET კვებავს CSRF ტოკენებს, ხელმოწერილ URL-ებს, remember-me cookie-ს და ყველაფერს, რასაც Symfony ხელს აწერს. შექმენი ერთხელ და ხელი აღარ ახლო, რადგან მისი შეცვლა ხელმოწერილ ბმულებს აბათილებს და მომხმარებლებს სისტემიდან გამოაგდებს. ერთ-ერთი გონივრული გზა მის შესაქმნელად:
$ php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;'ნაგულისხმევ გარემოს, როცა არაფერი აყენებს, .env-ის APP_ENV ხაზი განსაზღვრავს, recipe კი მას dev-ად აწვდის. სწორედ ეს უნდა შეამოწმო პირველ რიგში, როცა ახლად გაშვებული აპლიკაცია მთელ ინტერნეტს Symfony profiler toolbar-ს აჩვენებს: არცერთმა რეალურმა ცვლადმა ფაილი არ გადაფარა.
.env, .env.local და რეალური გარემოს ცვლადები#
Symfony-ის Dotenv კომპონენტი ფაილებს განსაზღვრული თანმიმდევრობით ტვირთავს და ამის გაგება ხსნის "რატომ იყენებს არასწორ ბაზას" ტიპის შეკითხვების უმეტესობას. ფაილები, ყველაზე დაბალი პრიორიტეტიდან ყველაზე მაღალისკენ:
| ფაილი | commit-დება? | დანიშნულება |
|---|---|---|
.env | დიახ | ნაგულისხმევი მნიშვნელობები და ყველა ცვლადის აღწერა |
.env.local | არა | კონკრეტული მანქანის გადაფარვები, APP_ENV=test-ის დროს არ იკითხება |
.env.prod | დიახ | prod გარემოს ნაგულისხმევი მნიშვნელობები |
.env.prod.local | არა | კონკრეტული მანქანის prod გადაფარვები |
ამ ყველაფრის ზემოთ დგას რეალური გარემოს ცვლადები: მნიშვნელობები, რომლებსაც PHP-ის გამშვები პროცესი აყენებს. Dotenv არასოდეს გადააწერს უკვე არსებულ ცვლადს, ამიტომ რეალური ცვლადი ყოველთვის იგებს ნებისმიერ ფაილთან. production-ში სწორედ ეს თვისება გჭირდება. ფაილები development-ის კომფორტისთვისაა, სიმართლის წყარო კი სერვერის საკუთარი გარემოა.
აქედან გამომდინარეობს:
.envcommit-ე მხოლოდ უსაფრთხო placeholder-ებით. ის ცვლადების სახელებს აღწერს და საბაზისო მნიშვნელობაა.- production-ის საიდუმლოებები
.env-ში არ ჩადო და.env.localარასოდეს commit-ო. რაც git-ში მოხვდა, გაჟონილად უნდა ჩათვალო. - RE:NODE-ზე production-ის მნიშვნელობები გარემოს ცვლადებად დააყენე სერვერის Startup ჩანართზე. ისინი რეალური ცვლადებია, ამიტომ ფაილებს გადაფარავს. გარემოს ცვლადები და საიდუმლოებები განმარტავს, რა სად უნდა ინახებოდეს და რა არ უნდა მოხვდეს ლოგში.
- თუ ფაილი გირჩევნია,
.env.localშექმენი სერვერზე ფაილ მენეჯერით ან SFTP-ით. ის მუშაობს, მაგრამ პროექტის დირექტორიაში დევს, ამიტომ არქივში, რომელსაც სხვას უგზავნი, არ ჩასვა.
არსებობს ორი Symfony-ის საკუთარი ალტერნატივა, რომლებიც იცოდე. პირველია composer dump-env prod, რომელიც .env ფაილებს ერთ .env.local.php მასივად აკომპილირებს; ის ყოველ სტარტზე ტექსტის დაპარსვის გარეშე იტვირთება. გაუშვი ის იმ მანქანაზე, რომელიც აპლიკაციას მოემსახურება, deploy-ის ნაწილად, და გახსოვდეს, რომ ის მნიშვნელობებს ყინავს: ცვლადი თუ შეცვალე, ხელახლა უნდა გააკეთო dump. მეორეა secrets vault, php bin/console secrets:set DATABASE_PASSWORD --env=prod, რომელიც დაშიფრულ მნიშვნელობებს config/secrets/prod/-ში ინახავს და მათი commit-ი შეიძლება. მაშინ სერვერზე მხოლოდ ერთი რეალური ცვლადია საჭირო - გაშიფვრის გასაღები SYMFONY_DECRYPTION_SECRET - ხოლო php bin/console secrets:decrypt-to-local --env=prod vault-ს ლოკალურ ფაილად აქცევს, როცა ეს გჭირდება.
Composer სტარტზე და production ინსტალაცია#
ამ აპლიკაციების ხაზზე Composer დამოკიდებულებებს სერვერის გაშვებისას აყენებს, ამიტომ ახალი composer.lock მომდევნო restart-ზე ამოქმედდება. Symfony-ის მხარეს წესი ყველგან ერთია: commit-ე composer.lock, რათა ინსტალაციამ ზუსტად ის ვერსიები აიღოს, რომლებიც შენ გატესტე. lock ფაილის გარეშე ყოველი სტარტი შეზღუდვებს თავიდან ხსნის და production-ში იმუშავებს ის ვერსია, რომელიც Packagist-მა იმ დილით შემოგთავაზა.
ბრძანება, რომელსაც production ინსტალაცია უნდა უტოლდებოდეს:
$ composer install --no-dev --optimize-autoloader --no-interaction--no-dev გამოტოვებს require-dev პაკეტებს: profiler-ს, maker bundle-ს, PHPUnit-ს, debug ხელსაწყოებს. --optimize-autoloader ქმნის class map-ს, რათა PHP-მ თითოეულ autoload-ზე ფაილური სისტემა არ დაათვალიეროს. Symfony აპლიკაციებში composer.json ასევე შეიცავს auto-scripts სექციას, რომელსაც recipe post-install-cmd-ში აერთებს, რათა cache:clear და assets:install public ყოველი ინსტალაციის შემდეგ გაეშვას. სწორედ ამიტომ არის ინსტალაცია და cache-ის ნაბიჯი ერთმანეთზე გადაბმული.
კლასიკური შეცდომა აქედან გამომდინარეობს. თუ Composer-ის დასრულებისას APP_ENV არ არის prod და dev პაკეტები არ არის, post-install cache:clear ცდილობს dev container-ის აგებას, ვერ პოულობს Symfony\Bundle\DebugBundle\DebugBundle-ს, რომელიც config/bundles.php-ის dev ჩანაწერებშია, და წყდება. დააყენე APP_ENV=prod გარემოში ინსტალაციის გაშვებამდე და არა მის შემდეგ და შეცდომა აღარ გამოჩნდება. თუ staging სერვერზე dev პაკეტები გჭირდება, დატოვე ისინი და იქ APP_ENV=dev განზრახ დააყენე.
პლატფორმა არ აღწერს, რომელ ფლაგებს იყენებს სტარტზე ინსტალაცია, ამიტომ ვარაუდის ნაცვლად პირველი გაშვებისას წაიკითხე კონსოლის გამოტანა. კონსოლი ინსტალაციის გამოტანას პირდაპირ და გაფილტრვის გარეშე აჩვენებს. კონსოლის კითხვა განმარტავს ისტორიას, დაკეცილ ხმაურს და მეხსიერების გრაფიკს, რომელიც პირველად გახსნილი გენდომება.
Front-end ასეტები მეორე build ნაბიჯია. AssetMapper-ით php bin/console asset-map:compile fingerprint-ებიან ფაილებს public/assets/-ში წერს. Webpack Encore-ით ან Vite-ით build გააკეთე საკუთარ მანქანაზე ან CI-ში და შედეგი ატვირთე: Node-ის დაყენება და დიდი bundle-ის აკომპილირება 1 GB-იან გეგმაზე მეხსიერების ლიმიტის მისაღწევად კარგი გზაა.
Cache clear და warmup#
production cache var/cache/prod/-ში ინახება. მასში არის აკომპილირებული service container, routing ცხრილი, აკომპილირებული Twig თარგები, validator-ისა და serializer-ის მეტამონაცემები და Doctrine proxy კლასები. ეს ყველაფერი გენერირებულია, არაფერი არ ეკუთვნის git-ს, ხოლო მოძველებული ასლი "დავდეპლოიე, მაგრამ არაფერი შეცვლილა" ტიპის საჩივრების გასაკვირად დიდი ნაწილის მიზეზია.
მნიშვნელოვანია ორი ბრძანება:
$ php bin/console cache:clear$ php bin/console cache:warmupprod გარემოში cache:clear cache-ს თავიდან აგებს და იმავე გაშვებაში აცხელებს, ამიტომ ჩვეულებრივ ერთი ბრძანება საკმარისია; cache:warmup არსებობს გასუფთავების გარეშე გასახურებლად, cache:clear-ზე --no-warmup კი warmup-ს გამოტოვებს. deploy-ში გახურების მიზეზი მარტივია: ცივი cache-ის შემდეგ პირველი მოთხოვნა container-ს აკომპილირებს და შეიძლება რამდენიმე წამი და რამდენიმე ასეული მეგაბაიტი მეხსიერება დასჭირდეს. მცირე გეგმაზე ამ ნახტომმა შეიძლება ლიმიტს თავისით მიაღწიოს.
ორი ჩვევა ამას მშვიდად გაატარებს:
- cache ააგე იქ, სადაც მოემსახურება. აკომპილირებული container აბსოლუტურ გზებს შეიცავს. შენს ლეპტოპზე აგებული და ატვირთული cache სერვერზე არარსებულ დირექტორიებზე მიუთითებს.
- მომხმარებლებს ნუ ურევ. თუ ერთი მომხმარებლის სახელით გაშვებულმა ბრძანებამ შექმნა
var/cache/prod/, ხოლო ვებ პროცესი სხვა მომხმარებლით მუშაობს, მეორეს მასში ჩაწერა არ შეუძლია. პანელის კონსოლი და სერვერის პროცესი container-ის ერთსა და იმავე მომხმარებელს იყენებს, რაც პრობლემას ერიდება; განსხვავებული მომხმარებლით SFTP სესიამ შეიძლება ის თავიდან შექმნას, ამიტომ ატვირთვის შემდეგ cache კონსოლიდან გაასუფთავე.
ასევე გაითვალისწინე, რომ cache-ში შენახული კონფიგურაცია არის მიზეზი, რის გამოც შეცვლილი გარემოს ცვლადი ზოგჯერ არაფერს აკეთებს. Symfony %env()% მნიშვნელობებს გაშვებისას არკვევს, ამიტომ ჩვეულებრივი ცვლადები cache-ში არ იწვება, მაგრამ ყველაფერი, რაც container-იდან კომპილირდება, მაგალითად services.yaml-ში გამოყვანილი პარამეტრი, იწვება. ეჭვის შემთხვევაში გაასუფთავე.
Doctrine migrations#
სქემის ცვლილებები მიდის migration კლასებად migrations/-ში, production-ის ბრძანება კი ასეთია:
$ php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration--no-interaction აჩერებს დადასტურების მოთხოვნას, რომელიც არაინტერაქტიულ deploy-ს გააჭედებდა. --allow-no-migration ბრძანებას წარმატებით ამთავრებს, როცა გასაშვები არაფერია, რაც deploy სკრიპტს იცავს ყველაზე ხშირ შემთხვევაში წარუმატებლობისგან: როცა რელიზს სქემის ცვლილებები არ აქვს. პირველ deploy-მდე და ყოველთვის, როცა ეჭვი გაქვს, doctrine:migrations:status გამოიტანს კონფიგურირებულ ბაზას, მიმდინარე ვერსიას და რამდენი migration ელოდება, ხოლო doctrine:schema:validate შენს mapping-ს ცოცხალ სქემასთან ადარებს.
doctrine:schema:update --force production-ში არასოდეს გამოიყენო. ის განსხვავებას ითვლის და ერთსავე ნაბიჯში ასრულებს, არაფერი გადამოწმებული და არაფერი ჩაწერილი არ არის. migration-ები development-ში შექმენი php bin/console doctrine:migrations:diff-ით, SQL წაიკითხე, commit-ე და production-ს მხოლოდ commit-ული migration-ები გაუშვი.
DATABASE_URL ცვლადი კავშირს ატარებს. სქემა ძრავს მისდევს, ხოლო host, port, user და password მოდის იქიდან, სადაც ბაზა შეიქმნა:
DATABASE_URL="postgresql://app:secret@db.example.net:5432/app?serverVersion=16&charset=utf8"MySQL-ისთვის სქემა mysql://-ია და serverVersion სერვერის რეალური ვერსიაა და არა ვარაუდი: Doctrine მას SQL დიალექტის შესაძლებლობების ასარჩევად იყენებს, არასწორი მნიშვნელობა კი migration-ებს უცნაურად ჩააგდებს. RE:NODE-ზე აპლიკაციის სერვერს აქვს ბაზის სლოტები, რომლებიც პანელში იქმნება გენერირებული host-ით, user-ითა და password-ით, ცალკეული ბაზების ხაზი კი PostgreSQL-ს, MySQL 8.4 LTS-სა და სხვებს საკუთარ host-სა და port-ზე ყიდის. მნიშვნელობები პანელიდან აიღე და password URL-encode გააკეთე, თუ ის შეიცავს @, / ან : სიმბოლოებს, წინააღმდეგ შემთხვევაში Doctrine URL-ს უაზრობად დაპარსავს. ბაზის backup-ები და აღდგენა backup-ის მხარეს განმარტავს.
თანმიმდევრობას მნიშვნელობა აქვს. რელიზი, რომლის კოდს ახალი სვეტი სჭირდება migration-ის გაშვებამდე, ყველა ვიზიტორს 500-ს აძლევს ამ ხარვეზის მთელ მონაკვეთზე. მცირე საიტზე მეტისთვის წერე უკუთავსებადი migration-ები: დაამატე სვეტი nullable-ად, გაუშვი კოდი, რომელიც ამას უძლებს, შეავსე მონაცემები და დააჭირე მომდევნო რელიზში. Migration-ები downtime-ის გარეშე ამ ხერხს აღწერს. ნებისმიერი migration-ის წინ, რომელიც რაღაცას შლის ან სახელს უცვლის, გააკეთე backup; RE:NODE-ზე Backups ჩანართი ამას მოთხოვნით აკეთებს და ღილაკით აღადგენს.
Messenger worker-ები, transport და scheduler#
ყველაფერი, რაც მოთხოვნაში ნელია - ფოსტა, სურათების დამუშავება, მესამე მხარეების გამოძახება - Messenger transport-ს ეკუთვნის, ხოლო transport-ს სჭირდება consumer პროცესი, რომელიც მოთხოვნას გადაურჩება. ჯერ .env-ში აირჩიე transport:
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0Doctrine transport შეტყობინებებს ბაზის ცხრილში ინახავს და დამატებით არაფერი სჭირდება. production-ში დააყენე auto_setup=0 და ცხრილი ერთხელ შექმენი php bin/console messenger:setup-transports-ით, რათა აპლიკაციამ გაშვებისას სქემის შეცვლა არ სცადოს. მეტი გამტარუნარიანობისთვის შემდეგი ნაბიჯია Valkey-ზე დაფუძნებული transport (DSN-ში redis://, რადგან Valkey Redis პროტოკოლს ლაპარაკობს) - იხილე job queue-ები Valkey-ით.
consumer ხანგრძლივად მომუშავე ბრძანებაა:
$ php bin/console messenger:consume async --time-limit=3600 --memory-limit=128Mორი ლიმიტი აქ მთავარია. --time-limit=3600 worker-ს ერთი საათის შემდეგ გამოაყვანს, --memory-limit=128M კი გამოიყვანს, როცა ამაზე მეტს გაიზრდება, რაც გიცავს კოდში ნელი გაჟონვისგან, რომელსაც ვერ აკონტროლებ. მოსალოდნელია, რომ supervisor მას ხელახლა გაუშვებს, container-ზე დაფუძნებულ deploy-ს კი ამაზე პასუხი თვითონ უნდა გაეცეს.
RE:NODE სერვერი არის ერთი container, რომელიც ერთ პროცესს უშვებს. პანელის ჩანართები ამაზეა აგებული, ამიტომ პატიოსანი გადაწყვეტაა consumer ცალკე სერვერად ჩათვალო: მეორე აპლიკაციის სერვერი იმავე ხაზზე, იგივე repository, იგივე გარემოს ცვლადები, იგივე ბაზა, გაშვებული ბრძანებით, რომელიც ვებ სერვერის ნაცვლად worker-ს უშვებს. შენს გეგმაზე Startup ჩანართზე შეამოწმე, შეიძლება თუ არა start ბრძანების შეცვლა, რადგან პლატფორმის ტექსტი იქ გარემოს ცვლადებს ადასტურებს და ბრძანების რედაქტირებას არ აღწერს. თუ არ შეიძლება, ალტერნატივებია messenger transport, რომელსაც პატარა დაგეგმილი კონსოლის ამოცანა მოიხმარს, ან დაბალი მოცულობისთვის sync transport, სანამ გადაწყვეტ. worker ფონურ პროცესად ვებ სერვერის საკუთარ კონსოლის სესიაში ნუ გაუშვებ და ნუ ელი, რომ restart-ს გადაურჩება.
ყოველი deploy-ის შემდეგ consumer ძველ კოდს მეხსიერებაში ინახავს. უთხარი, გაჩერდეს, php bin/console messenger:stop-workers-ით, რომელიც cache pool-ში დროშას ყენებს; თითოეული worker მიმდინარე შეტყობინებას ამთავრებს და გამოდის, restart კი მას ახალი კოდით აბრუნებს. წარუმატებელი შეტყობინებები მიდის failure transport-ში, თუ ის გაქვს კონფიგურირებული, და შემდეგ:
$ php bin/console messenger:failed:show$ php bin/console messenger:failed:retryიგივე ხანგრძლივად მომუშავე მექანიზმი უშვებს Symfony Scheduler-ს: განმეორებადი შეტყობინებები აღწერე #[AsPeriodicTask]-ით ან schedule provider-ით და scheduler_default transport მოიხმარე messenger:consume scheduler_default-ით. ის კონსოლის ბრძანებების crontab-ს ერთი worker-ით ცვლის. თუ უბრალო cron გამოსახულებები გირჩევნია, cron გამოსახულებების ახსნა ხუთ ველს განმარტავს, ფონური ამოცანები პატარა სერვერზე კი ამ გზამკვლევში ნავარაუდები ზომის მანქანისთვის ვარიანტებს ადარებს.
var/ ჩაწერადი უნდა იყოს და სად მიდის ლოგები#
Symfony გაშვებისას წერს ზუსტად ერთ ხეში: var/. მასში var/cache/ აკომპილირებულ cache-ს ინახავს, var/log/ ლოგ ფაილებს, ხოლო სესიები var/sessions/-ში ხვდება, თუ ფაილურ handler-ს იყენებ. ვებ პროცესს იქ ფაილების შექმნა და ჩაწერა უნდა შეეძლოს. პროექტში სხვას არაფერს სჭირდება ჩაწერის უფლება, მათ შორის src/, config/ და vendor/-ს.
var/-ის არაჩაწერადობის სიმპტომი ერთხელ ნახვის შემდეგ ცალსახაა: 500 შეცდომა და კონსოლში ან var/log-ში შეტყობინება, რომელიც იწყება Unable to create the "cache" directory ან Failed to write file-ით. გამოსწორება მფლობელის შესწორებაა და არა უფლებების 777-მდე გაფართოება:
$ chmod -R ug+rwX varლოგინგზე გადაწყვეტილება უნდა მიიღო. recipe-ის Monolog კონფიგურაცია prod-ისთვის ჩანაწერებს fingers_crossed handler-ით ბუფერავს, რომელიც მთელი მოთხოვნის კონტექსტს მხოლოდ მაშინ წერს, როცა error დონის ჩანაწერი მოვა, და შედეგს JSON-ად php://stderr-ზე აგზავნის. container-ზე ეს სწორი ნაგულისხმევია, რადგან stderr პირდაპირ კონსოლში მიდის, სადაც ცოცხლად კითხულობ, და დისკს არაფერი ავსებს. თუ handler-ს ხანგრძლივი შენახვისთვის var/log/prod.log-ის stream-ზე გადართავ, ჩართე როტაცია: Monolog-ის rotating_file handler max_files მნიშვნელობით ლოგს აჩერებს, სანამ გეგმის დისკი არ აივსება. ლოგები, რომლებიც ღირს შენახვად განიხილავს, რა და რამდენ ხანს შეინახო.
ატვირთვები ცალკე საკითხია. ფაილები, რომლებსაც მომხმარებლები ტვირთავენ, უნდა ინახებოდეს იქ, სადაც backup-ს გააკეთებ, და არა var/-ში, რომელიც განზრახ ჩასაგდებია. განათავსე ისინი cache ხის გარეთ გზაზე ან S3-თავსებად object storage-ში და ეს გზა შეიყვანე backup გეგმაში. backup, რომელიც არავის აღუდგენია, ჰიპოთეზაა.
Proxy slot-ის უკან: trusted proxies და X-Forwarded-For#
თითქმის ყველა production Symfony აპლიკაცია რაღაცის უკან დგას, რაც TLS-ს ამთავრებს. RE:NODE-ზე აპლიკაციის გეგმებს აქვს proxy slot: A ჩანაწერი მიუთითე მისამართზე, რომელსაც პანელი აჩვენებს, და სერტიფიკატი შენთვის გაიცემა და განახლდება 21-დღიან ფანჯარაში. რეალური კლიენტის IP მოდის X-Forwarded-For header-ში. რას აკეთებს reverse proxy გზას განმარტავს, HTTPS და Let's Encrypt ახსნილი კი იმას, რატომ ვერ გაიცემა სერტიფიკატი, როცა DNS მზად არ არის.
Symfony-სთვის შედეგი ისაა, რომ ნაგულისხმევად ის გადამისამართებულ header-ებს არ ენდობა. კონფიგურაციის გარეშე $request->getClientIp() proxy-ს მისამართს აბრუნებს, $request->isSecure() false-ია და UrlGeneratorInterface HTTPS გვერდზე http:// ბმულებს აგებს, რომლებსაც ბრაუზერები mixed content-ად ბლოკავენ. rate limiter-ები და ავტორიზაციის throttling-ი კი ყველა ვიზიტორს ერთ კლიენტად ითვლის, რაც უარესია, ვიდრე ჟღერს.
უთხარი Symfony-ს, რომელ proxy-ებს ენდოს და რომელი header-ები წაიკითხოს, config/packages/framework.yaml-ში:
framework: trusted_proxies: '%env(TRUSTED_PROXIES)%' trusted_headers: - 'x-forwarded-for' - 'x-forwarded-host' - 'x-forwarded-proto' - 'x-forwarded-port' - 'x-forwarded-prefix'შემდეგ დააყენე ცვლადი. Symfony იღებს მძიმით გამოყოფილ IP-ებისა და CIDR დიაპაზონების სიას, ასევე ორ სპეციალურ მნიშვნელობას: PRIVATE_SUBNETS ენდობა loopback-ს და კერძო დიაპაზონებს, REMOTE_ADDR კი ენდობა ნებისმიერ მისამართს, საიდანაც კავშირი მოდის.
TRUSTED_PROXIES=PRIVATE_SUBNETSკომპრომისი აქ მთავარია. REMOTE_ADDR სწორია მხოლოდ მაშინ, როცა proxy აპლიკაციაში შესვლის ერთადერთი გზაა, რადგან ნებისმიერს, ვისაც დაკავშირება შეუძლია, მაშინ X-Forwarded-For-ის გაყალბება და ვინმედ მოჩვენება შეეძლება. პლატფორმის ტექსტი proxy-ს საკუთარ მისამართს არ ასახელებს, ამიტომ საიმედო მეთოდი ემპირიულია: გაუშვი route, რომელიც $request->server->get('REMOTE_ADDR')-ს ბეჭდავს, სანამ შენი ანგარიშით ხარ შესული, წაიკითხე მნიშვნელობა და TRUSTED_PROXIES-ში wildcard-ის ნაცვლად ეს მისამართი ან დიაპაზონი ჩაწერე. გახსოვდეს, რომ trusted proxy-ს უკან header-ი მისამართების ჯაჭვს შეიძლება შეიცავდეს და Symfony ბოლო არადამაჯერებელს ირჩევს, ამიტომ სია ზუსტი უნდა იყოს და არა უბრალოდ ფართო.
დააყენე ასევე DEFAULT_URI ან router-ის default_uri (უფრო ძველი ფორმაა router.request_context.host და scheme პარამეტრები), რათა კონსოლის ბრძანებებსა და Messenger handler-ებში გენერირებულმა URL-ებმა, რომლებს scheme-ის წასაკითხი მოთხოვნა არ აქვთ, https:// და შენი რეალური hostname გამოიყენონ.
Health route და რას უნდა უყურო#
Symfony-ს ჩაშენებული health endpoint არ აქვს, რაც ადვილი გამოსასწორებელია და ღირს გასწორება. კარგი endpoint სწრაფად პასუხობს, არაფერ ნელს არ ეხება და სურვილისამებრ ბაზას ცალკე ამოწმებს:
<?phpnamespace App\Controller;use Doctrine\DBAL\Connection;use Symfony\Component\HttpFoundation\JsonResponse;use Symfony\Component\Routing\Attribute\Route;final class HealthController{ #[Route('/healthz', name: 'health', methods: ['GET'])] public function live(): JsonResponse { return new JsonResponse(['status' => 'ok']); } #[Route('/healthz/db', name: 'health_db', methods: ['GET'])] public function db(Connection $connection): JsonResponse { $connection->executeQuery('SELECT 1'); return new JsonResponse(['status' => 'ok']); }}liveness route ბაზის გამოძახებების გარეშე დატოვე, რათა ბაზის მცირე შეფერხებამ monitor-ს არ აფიქრებინოს, რომ ვებ პროცესი მკვდარია. დარწმუნდი, რომ შენი firewall კონფიგურაცია /healthz-ს საჯაროს ტოვებს (მაგალითად ^/healthz access_control-ში PUBLIC_ACCESS-ით) და რომ ის locale პრეფიქსში არ ხვდება. Graceful shutdown და health check-ები განმარტავს, რა უნდა შეამოწმო და რატომ.
პანელის საკუთარი ხედი ისევე მნიშვნელოვანია. კონსოლის გრაფიკი მეხსიერებას, CPU-სა და დისკს გეგმის ლიმიტებთან ერთად აჩვენებს და RE:NODE-ს PHP სერვისისთვის ორი ქცევა აქვს, რომელიც უნდა იცოდე. მეხსიერების ლიმიტზე ბირთვი container-ს აჩერებს და ის swap-ის ნაცვლად სუფთად იწყება თავიდან, ამიტომ გაჟონვა ან გადაჭარბებული cache warmup აპლიკაციის ლოგში არაფრით ჩანს, უბრალოდ restart-ად გამოჩნდება. გარდა ამისა, watcher ყოველ ორ წუთში ამოწმებს, ხომ არ დაბრუნდა uptime უკან: ერთ საათში სამი restart სერვერის გვერდზე გაფრთხილებას და ავტომატურ ticket-ს იწვევს, ექვსი კი შეჩერებას. ცუდი deploy-ის გამო crash loop-ი, მაგალითად ნაკლული migration, ამიტომ უნდა გააჩერო და სწრაფად გამოასწორო და არა უყურო.
გეგმა გაზომვით შეარჩიე და არა იმედით. prod რეჟიმში Symfony აპლიკაცია worker mode-ის გარეშე ჩვეულებრივ ათეულობით მეგაბაიტს მოიხმარს მოთხოვნაზე, ცივი cache-ით პირველი მოთხოვნა კი რამდენჯერმე მეტს; ვებ აპლიკაციის ზომის შერჩევა გაშვების დღისთვის ამას მეთოდად აქცევს, PHP-ის პარამეტრები, რომლებსაც მნიშვნელობა აქვს კი memory_limit-სა და OPcache-ს ეხება.
Deploy: git, SFTP და პირველი სტარტის გვერდი#
კოდი სერვერამდე ორი გზით აღწევს და ორივე პანელზეა.
- Git deploy. მხოლოდ GitHub, GitHub App-ის გავლით მოკლევადიანი ტოკენებით, ამიტომ კერძო repository-ები მუშაობს. ორი გადამრთველი: pull ყოველ სტარტზე და deploy push-ზე, რომელიც გაშვებულ სერვერს ხელახლა უშვებს, გაჩერებულს კი ჩართავს. ყოველ deploy-ს საკუთარი ჩანაწერი აქვს. რადგან Composer სტარტზე აყენებს, ეს კომბინაცია გაძლევს "push, restart, ახალი დამოკიდებულებები" სქემას build სერვერის გარეშე.
- ფაილ მენეჯერი ან SFTP. SFTP მონაცემები თითო სერვერზეა და არა ანგარიშზე. ფაილ მენეჯერი არქივებს ადგილზე ხსნის, რაც დაზიპული რელიზის ასატვირთად მოსახერხებელია. შენი მანქანიდან
vendor/,var/ან.env.localარ ატვირთო.
ამ ხაზის ახალი სერვერი თავის მისამართზე პატარა სამუშაო გვერდით იწყებს, სანამ კოდს არ გაუშვებ, და ეს გვერდი არასოდეს გადააწერს შენ მიერ ატვირთულ ფაილებს, ამიტომ მისამართის პირველი ნახვა placeholder-ია და არა შენი აპლიკაცია. როცა კოდი იქ არის, deploy თანმიმდევრობა, რომელსაც ყოველთვის ერთნაირად გაუშვებ, ასე გამოიყურება:
$ composer install --no-dev --optimize-autoloader --no-interaction$ php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration$ php bin/console cache:clear$ php bin/console messenger:stop-workersგაუშვი ის პანელის კონსოლიდან, სადაც შესაძლებელია, და ყოველ ჯერზე ნაბიჯები ერთსა და იმავე რიგში შეინარჩუნე: ინსტალაცია, migration, cache-ის აგება, worker-ების restart. თუ migration რაღაცას შლის, ჯერ backup გააკეთე. deploy-სთვის, რომელიც ვიზიტორებისთვის უჩინარია და არა უბრალოდ სწრაფი, zero-downtime deploy-ები პატარა სერვერზე release-directory სქემას აღწერს, იმ დათქმით, რომ ერთი container-ის გეგმაზე მას მეტი დისკი და მეტი სიფრთხილე სჭირდება.
პრობლემების აღმოფხვრა#
ცარიელი გვერდი ან უბრალო 500 production-ში. ეს APP_DEBUG=0-ის საქმეა. ჯერ წაიკითხე კონსოლის გამოტანა, რადგან Monolog შეცდომებს stderr-ზე აგზავნის, შემდეგ var/log/prod.log, თუ handler შეცვალე. თუ ორივე ცარიელია, PHP-ს მათი ჩაწერა ვერ შეძლო: შეამოწმე var/-ის უფლებები.
profiler toolbar ჩნდება ან გამონაკლისის გვერდი ფაილების გზებს აჩვენებს. APP_ENV ან APP_DEBUG ჯერ კიდევ development-ის მნიშვნელობაა. დააყენე APP_ENV=prod და APP_DEBUG=0 რეალურ ცვლადებად და შეამოწმე, რომ არცერთი უცხო .env.local.php მათ არ გადაფარავს.
"Unable to create the cache directory" ან "Failed to write file". var/ არ არის ჩაწერადი იმ პროცესისთვის, რომელიც PHP-ს უშვებს, ან ადრინდელმა ბრძანებამ var/cache სხვა მომხმარებლით შექმნა. გაასწორე მფლობელი, შემდეგ კონსოლიდან გაუშვი cache:clear.
"Class DebugBundle not found" deploy-ის შემდეგ. dev პაკეტები --no-dev-ით მოიხსნა, მაგრამ container dev გარემოში აიგო. დააყენე APP_ENV=prod Composer-ის გაშვებამდე.
ყველაფერი მუშაობს, გარდა ბმულებისა, რომლებიც http://-ია, და ავტორიზაციის redirect loop-ებისა. trusted proxies კონფიგურირებული არ არის, ამიტომ Symfony HTTPS-ს ვერ ხედავს. დააყენე TRUSTED_PROXIES და ზემოთ ნაჩვენები trusted_headers სია.
გარემოს ცვლადის ცვლილება არაფერს აკეთებს. რეალურ ცვლადს PHP-მდე მისაღწევად სერვერის restart სჭირდება. თუ composer dump-env-საც იყენებ, აკომპილირებული .env.local.php ძველ მნიშვნელობას ინახავს. ხელახლა გააკეთე dump ან წაშალე.
"An exception occurred in the driver: could not translate host name". DATABASE_URL-ის host არასწორია ან password-ში დაუშიფრავი სპეციალური სიმბოლოებია. host და port პანელიდან გადმოწერე და password დაშიფრე.
შეტყობინებები რიგში დგას და არავინ მოიხმარს. consumer არ მუშაობს ან ძველ კოდზე მუშაობს. გაუშვი ერთი და messenger:stop-workers ყოველი deploy-ის ნაწილად აქციე, რათა ხანგრძლივად მომუშავე worker ჩანაცვლდეს.
სერვერი თავისით restart-დება, ლოგში არაფრით. მეხსიერების ლიმიტი. შეხედე მეხსიერების გრაფიკს restart-ის დროისთვის. შეამცირე worker-ზე --memory-limit, cache გაახურე deploy-ში და არა პირველ მოთხოვნაზე და წაიკითხე Linux swap და OOM killer, რომ გაიგო, რას ნიშნავს ბირთვის გაჩერება.
FAQ#
მჭირდება თუ არა worker mode Symfony-ს FrankenPHP-ზე გასაშვებად?
არა. FrankenPHP Symfony-ს ჩვეულებრივ რეჟიმში ემსახურება, ერთი ჩატვირთვა მოთხოვნაზე, ზუსტად ისე, როგორც PHP-FPM. worker mode შესრულების ოპცია, რომელიც kernel-ს მოთხოვნებს შორის მეხსიერებაში ინახავს. ჩართე ის მას შემდეგ, რაც აპლიკაცია სტაბილურია, და გატესტე state-ის გადადინება ერთი მოთხოვნიდან მეორეში.
სად უნდა ინახებოდეს production-ის საიდუმლოებები: .env-ში თუ რეალურ გარემოს ცვლადებში?
რეალურ გარემოს ცვლადებში ან Symfony secrets vault-ში. .env commit-ულია და სახელებს აღწერს. რეალური ცვლადი ყოველთვის იგებს ნებისმიერ ფაილთან, რადგან Dotenv არსებულ მნიშვნელობებს არ გადააწერს. RE:NODE-ზე გარემოს ცვლადების ადგილი Startup ჩანართია.
უსაფრთხოა თუ არა composer install --no-dev Symfony აპლიკაციისთვის?
დიახ, სანამ production კოდში არაფერი ეყრდნობა dev პაკეტს და APP_ENV=prod ინსტალაციამდეა დაყენებული. თუ "class not found" შეცდომა პაკეტს ასახელებს, ის require-ში უნდა იყოს და არა require-dev-ში. Maker, profiler და PHPUnit require-dev-ში უნდა დარჩეს.
რატომ ხედავს Symfony proxy-ს IP-ს ვიზიტორის ნაცვლად?
რადგან ის გადამისამართებულ header-ებს არ ენდობა, სანამ არ უთხრეს. დააყენე framework.trusted_proxies proxy-ს მისამართზე ან დიაპაზონზე, x-forwarded-* header-ები trusted_headers-ში ჩამოთვალე და getClientIp() X-Forwarded-For-ის მისამართს დააბრუნებს. თუ შენს origin-ს პირდაპირ მისვლა შეიძლება, ყველაფრისადმი ნდობას ერიდე.
Doctrine migration-ები deploy-ზე ავტომატურად გაეშვება?
თავისით არა. doctrine:migrations:migrate ნაბიჯად თვითონ უნდა გაუშვა და --no-interaction-ით არაინტერაქტიული გახადო. გაუშვი ის ახალი კოდის ტრაფიკის მიღებამდე, როცა ცვლილება უკუთავსებადი არ არის, და დამანგრეველი migration-ების წინ backup გააკეთე.
შეიძლება თუ არა Messenger worker იმავე სერვერზე მუშაობდეს, რაზეც ვებსაიტი?
სერვერი არის ერთი container ერთი პროცესით, ამიტომ worker დაგეგმე მეორე სერვერად იმავე კოდით, ცვლადებითა და ბაზით. შეიძლება თუ არა გეგმის start ბრძანების შეცვლა, ამ განლაგებამდე Startup ჩანართზე დაადასტურე.


