{"id":53,"date":"2026-03-31T08:36:54","date_gmt":"2026-03-31T08:36:54","guid":{"rendered":"https:\/\/kr0311.com\/projects\/?p=53"},"modified":"2026-04-02T14:14:41","modified_gmt":"2026-04-02T14:14:41","slug":"phase-6-control-panel-web-stack-provisioning","status":"publish","type":"post","link":"https:\/\/kr0311.com\/projects\/phase-6-control-panel-web-stack-provisioning\/","title":{"rendered":"Phase 6: Web Stack Provisioning"},"content":{"rendered":"<p>Phase 5 gave the custom hosting control panel build the ability to provision the underlying account layer safely.<\/p>\n<p>Linux users could be created, home directories could be built, and the application finally had a clean bridge into controlled system-level operations.<\/p>\n<p>This phase builds directly on <a href=\"\/projects\/phase-5-custom-hosting-control-panel-build\/\">Phase 5<\/a>, where the provisioning engine and system integration layer were introduced.<\/p>\n<p>Phase 6 is where that groundwork stopped being theoretical and started becoming genuinely useful.<\/p>\n<p>This phase pushed the panel into the live web stack itself.<\/p>\n<p>Instead of stopping at account creation, the system now provisions the pieces required to actually host a website: Nginx vhosts, PHP-FPM pools, document root alignment, configuration validation, and safe service reloads.<\/p>\n<p>In other words, this is the phase where a site stopped being just a database record and started becoming something the server could actually serve.<\/p>\n<hr \/>\n<h2>\u2699\ufe0f Web Stack Provisioning in the Custom Hosting Control Panel Build<\/h2>\n<p>The goal of this phase was straightforward, even if the implementation was not.<\/p>\n<p>When a site is created through the panel, the system now carries that process through the web stack layer as well.<\/p>\n<p>That means generating the correct Nginx virtual host, preparing PHP-FPM to run under the correct account context, linking the domain to the right document root, validating configuration before anything gets reloaded, and rolling back cleanly if something goes wrong.<\/p>\n<p>This moved the KR0311 Control Panel beyond internal provisioning and into real hosting orchestration.<\/p>\n<ul>\n<li><strong>Nginx vhost configs<\/strong> are generated dynamically<\/li>\n<li><strong>PHP-FPM pools<\/strong> are created for hosted accounts<\/li>\n<li><strong>Document roots<\/strong> are aligned to the correct site path<\/li>\n<li><strong>Config validation<\/strong> happens before reloads<\/li>\n<li><strong>Rollback logic<\/strong> prevents bad configs from taking the stack down<\/li>\n<\/ul>\n<p>So yes \u2014 the panel is now doing real hosting work.<\/p>\n<p>Which is exactly when you find out whether your architecture was solid or just well-dressed optimism.<\/p>\n<figure>\n    <img decoding=\"async\" src=\"https:\/\/kr0311.com\/projects\/wp-content\/uploads\/2026\/04\/Phase-6-sites-screenshot.png\" alt=\"custom hosting control panel build web stack provisioning sites panel\" loading=\"lazy\"><figcaption>\n        Sites panel after web stack provisioning, showing active domains, hosting accounts, and document roots managed by the control panel.<br \/>\n    <\/figcaption><\/figure>\n<p>At this point, the panel is no longer just storing site records \u2014 it is actively managing live hosting configurations, as shown in the sites panel after provisioning.<\/p>\n<hr \/>\n<h2>\ud83c\udf10 Nginx Provisioning<\/h2>\n<h3>\ud83e\udde9 Turning Site Records Into Real Vhosts<\/h3>\n<p>Phase 6 introduced dynamic Nginx vhost generation as part of site provisioning.<\/p>\n<p>Instead of manually writing server blocks every time a new domain was added, the panel now creates them automatically from the application layer using the provisioning system introduced in Phase 5.<\/p>\n<p>Generated configs are written into the expected Nginx structure:<\/p>\n<ul>\n<li><strong>\/etc\/nginx\/sites-available\/<\/strong><\/li>\n<li><strong>\/etc\/nginx\/sites-enabled\/<\/strong><\/li>\n<\/ul>\n<p>That keeps the resulting layout familiar, maintainable, and consistent with standard Nginx hosting workflows.<\/p>\n<p>The implementation also follows the same general principles described in the <a href=\"https:\/\/nginx.org\/en\/docs\/\" target=\"_blank\" rel=\"noopener\">official Nginx documentation<\/a>, which made it easier to keep the generated structure predictable rather than inventing something clever and regrettable.<\/p>\n<h3>\u2705 Validation Before Reload<\/h3>\n<p>This phase did not take the reckless route of writing config and hoping for the best.<\/p>\n<p>Every generated Nginx configuration is validated before reload.<\/p>\n<p>The provisioning flow writes the vhost file, creates the enablement symlink, runs <strong>nginx -t<\/strong>, and only proceeds with a reload if the configuration passes.<\/p>\n<p>If validation fails, the panel rolls the change back instead of leaving the server in a broken state.<\/p>\n<p>That sounds obvious on paper, but it is the difference between a provisioning system and a chaos generator.<\/p>\n<h3>\u21a9\ufe0f Rollback on Failure<\/h3>\n<p>A failed config does not just stop the process. It triggers cleanup.<\/p>\n<p>Bad vhost files and broken enablement links are removed, the failure is logged properly, and the related site status is marked accordingly.<\/p>\n<p>That ensures the application state and the system state stay aligned, which becomes more important with every new moving part added to the panel.<\/p>\n<hr \/>\n<h2>\ud83d\udc18 PHP-FPM Integration<\/h2>\n<h3>\ud83d\udc64 Dedicated Execution Context Per Hosting Account<\/h3>\n<p>Phase 6 also extended provisioning into PHP-FPM.<\/p>\n<p>When a site is created, the system now provisions a PHP-FPM pool tied to the hosting account user.<\/p>\n<p>Pool config files are generated into the standard PHP-FPM pool directory under <strong>\/etc\/php\/8.3\/fpm\/pool.d\/<\/strong>, and the pools run under the hosting account context rather than a generic shared execution model.<\/p>\n<p>That approach keeps the design aligned with the rest of the project, where hosting accounts already map cleanly to their own Linux user and home structure.<\/p>\n<p>It also lines up with the behaviour described in the <a href=\"https:\/\/www.php.net\/manual\/en\/install.fpm.php\" target=\"_blank\" rel=\"noopener\">PHP-FPM documentation<\/a>, which helped keep the pool structure grounded in something sensible rather than home-grown chaos.<\/p>\n<h3>\u2696\ufe0f Why Per-Account Pools Made Sense<\/h3>\n<p>One of the decisions in this phase was whether PHP-FPM should be split per site or per hosting account.<\/p>\n<p>The chosen direction was the more practical one for this stage of the project: a dedicated pool per hosting account.<\/p>\n<p>That gave the panel strong enough isolation for the current architecture without introducing unnecessary overhead or turning the server into a graveyard of hyper-fragmented pool definitions.<\/p>\n<p>It matched the account-based hosting model already in place, kept provisioning cleaner, and made the execution structure easier to reason about.<\/p>\n<p>For this version of the panel, that was the right balance between isolation, maintainability, and scalability.<\/p>\n<h3>\u2705 Validation Before Reload<\/h3>\n<p>Just like Nginx, PHP-FPM provisioning follows a validation-first pattern.<\/p>\n<p>The pool file is rendered and written, the PHP-FPM configuration is tested, and only then is the service reloaded.<\/p>\n<p>If the validation fails, the panel removes the bad pool config and marks the provisioning attempt as failed.<\/p>\n<p>No blind reloads. No wishful thinking. No \u201cit\u2019ll probably be fine.\u201d<\/p>\n<hr \/>\n<h2>\ud83e\udde0 Service Architecture<\/h2>\n<h3>\ud83d\udd27 Extending the Phase 5 Pattern<\/h3>\n<p>Phase 6 did not throw away the architecture established in Phase 5.<\/p>\n<p>It extended it.<\/p>\n<p>The provisioning model remained service-led, with the application layer orchestrating the workflow and the system layer performing tightly controlled actions through safe command execution.<\/p>\n<p>New services introduced in this phase included:<\/p>\n<ul>\n<li><strong>SiteProvisioningService<\/strong><\/li>\n<li><strong>NginxProvisioningService<\/strong><\/li>\n<li><strong>PHP-FPM Provisioning Service<\/strong> or equivalent<\/li>\n<\/ul>\n<p>These services sit on top of the existing <strong>SystemCommandService<\/strong>, which remains responsible for command execution via Symfony Process.<\/p>\n<p>The controller layer still does not directly run system commands.<\/p>\n<p>That separation remains one of the most important architectural rules in the entire project.<\/p>\n<h3>\ud83e\uddf1 Keeping the Boundaries Clean<\/h3>\n<p>The panel continues to follow a clear split between responsibilities.<\/p>\n<ul>\n<li><strong>Controllers<\/strong> handle request flow and persistence entry points<\/li>\n<li><strong>Jobs<\/strong> handle asynchronous provisioning execution<\/li>\n<li><strong>Services<\/strong> orchestrate system work<\/li>\n<li><strong>SystemCommandService<\/strong> executes privileged commands safely<\/li>\n<\/ul>\n<p>That structure keeps the codebase easier to reason about and makes the provisioning workflow far safer than stuffing server operations directly into request handlers and hoping the app survives its own enthusiasm.<\/p>\n<hr \/>\n<h2>\ud83d\udea8 Async Provisioning: The Critical Fix<\/h2>\n<h3>\ud83d\udca5 Why the Original Approach Failed<\/h3>\n<p>This was the most important correction in Phase 6.<\/p>\n<p>Originally, site provisioning was triggered synchronously inside the controller request lifecycle.<\/p>\n<p>On the surface, that seemed convenient. The request came in, the site was created, the system changes ran, and everything happened in one go.<\/p>\n<p>In practice, it was a terrible idea.<\/p>\n<p>Phase 6 provisioning performs live changes against the same web stack that serves the panel itself.<\/p>\n<p>That includes:<\/p>\n<ul>\n<li><strong>PHP-FPM pool creation<\/strong><\/li>\n<li><strong>PHP-FPM reloads<\/strong><\/li>\n<li><strong>Nginx vhost creation<\/strong><\/li>\n<li><strong>Nginx validation<\/strong><\/li>\n<li><strong>Nginx reloads<\/strong><\/li>\n<\/ul>\n<p>Trying to do all of that synchronously during the request led to intermittent <strong>502 Bad Gateway<\/strong> errors and general panel instability.<\/p>\n<p>Which, to be fair, is a pretty solid sign that the application does not enjoy hot-swapping the floor beneath its own feet.<\/p>\n<h3>\u2705 The Correct Architecture<\/h3>\n<p>The fix was to move provisioning out of the request cycle entirely.<\/p>\n<p>That decision fundamentally changed the Phase 6 architecture for the better.<\/p>\n<p>The final request flow now works like this:<\/p>\n<ul>\n<li><strong>Controller creates the site record<\/strong><\/li>\n<li><strong>Controller dispatches a ProvisionSite job<\/strong><\/li>\n<li><strong>The UI returns immediately<\/strong><\/li>\n<li><strong>The queue worker processes provisioning asynchronously<\/strong><\/li>\n<li><strong>Provisioning services perform the live system work<\/strong><\/li>\n<\/ul>\n<p>This is not just a bug fix.<\/p>\n<p>It is a production-grade architectural decision.<\/p>\n<p>Anything that modifies live services now happens outside the request thread, which protects the application from self-inflicted downtime and gives the provisioning layer a much safer execution model.<\/p>\n<h3>\ud83d\udd04 Queue Worker and Operational Stability<\/h3>\n<p>Once provisioning moved into jobs, queue processing became part of the actual control plane architecture rather than a nice extra.<\/p>\n<p>The project now uses a Redis-backed queue, with a dedicated systemd-managed worker service to ensure provisioning continues across reboots and does not depend on someone leaving a terminal session open all day.<\/p>\n<p>That was an important operational maturity step.<\/p>\n<p>A queue-driven provisioning system without a persistent worker is just a fancy way of creating stuck records.<\/p>\n<hr \/>\n<h2>\ud83d\udee1\ufe0f Safe Reload Strategy<\/h2>\n<h3>\ud83c\udf10 Nginx<\/h3>\n<p>The Nginx flow is intentionally strict:<\/p>\n<ul>\n<li><strong>Write config<\/strong><\/li>\n<li><strong>Enable site<\/strong><\/li>\n<li><strong>Run nginx -t<\/strong><\/li>\n<li><strong>If valid, reload<\/strong><\/li>\n<li><strong>If invalid, roll back<\/strong><\/li>\n<\/ul>\n<p>This ensures a broken vhost never gets promoted into the live stack unchecked.<\/p>\n<h3>\ud83d\udc18 PHP-FPM<\/h3>\n<p>The PHP-FPM flow follows the same philosophy:<\/p>\n<ul>\n<li><strong>Write pool config<\/strong><\/li>\n<li><strong>Validate PHP-FPM configuration<\/strong><\/li>\n<li><strong>If valid, reload<\/strong><\/li>\n<li><strong>If invalid, remove the bad config and fail cleanly<\/strong><\/li>\n<\/ul>\n<p>The key theme of Phase 6 was that service reloads are now treated as controlled operations, not blind side effects.<\/p>\n<hr \/>\n<h2>\ud83d\udcca Status System for Sites<\/h2>\n<p>Sites now have a proper provisioning lifecycle tied to real system outcomes.<\/p>\n<p>The status values used in Phase 6 are:<\/p>\n<ul>\n<li><strong>provisioning<\/strong><\/li>\n<li><strong>active<\/strong><\/li>\n<li><strong>failed<\/strong><\/li>\n<\/ul>\n<p>A newly created site enters the provisioning state first.<\/p>\n<p>If provisioning completes successfully, it becomes active. If the system work fails, it becomes failed.<\/p>\n<p>This matters because the panel is no longer dealing with hypothetical site entries.<\/p>\n<p>It is now managing live infrastructure state, and the application needs to reflect that honestly.<\/p>\n<hr \/>\n<h2>\ud83e\uddfe Logging and Visibility<\/h2>\n<p>The dedicated provisioning log introduced in Phase 5 was expanded in this phase to provide much better visibility into what the system is doing.<\/p>\n<p><strong>storage\/logs\/provisioning.log<\/strong> now captures far more than just account-level actions.<\/p>\n<p>Phase 6 logging includes:<\/p>\n<ul>\n<li><strong>Nginx config generation<\/strong><\/li>\n<li><strong>PHP-FPM pool creation<\/strong><\/li>\n<li><strong>validation results<\/strong><\/li>\n<li><strong>reload attempts<\/strong><\/li>\n<li><strong>failures and rollback actions<\/strong><\/li>\n<\/ul>\n<p>That visibility became especially important once provisioning moved into asynchronous jobs.<\/p>\n<p>When work is happening outside the request lifecycle, good logging stops being helpful and starts being essential.<\/p>\n<hr \/>\n<h2>\u26a0\ufe0f Error Handling<\/h2>\n<p>Phase 6 kept the same design principle established earlier in the build: system failures must not turn into ugly application failures.<\/p>\n<p>If provisioning hits a problem, the panel does not dump raw system output into the UI and it does not implode because a config test failed somewhere under the hood.<\/p>\n<p>Instead, the failure is logged properly, rollback happens where required, the site status is updated, and the user-facing experience stays controlled and readable.<\/p>\n<p>That kind of behaviour tends to feel boring when it works, which is exactly the point.<\/p>\n<hr \/>\n<h2>\ud83e\udde0 Challenges and Decisions<\/h2>\n<h3>\ud83d\udd04 Sync vs Async<\/h3>\n<p>The biggest challenge in this phase was not generating config files.<\/p>\n<p>It was respecting the reality of what happens when an application starts reloading the same services that keep it alive.<\/p>\n<p>The move from synchronous provisioning to queued jobs was the defining technical decision of the phase.<\/p>\n<p>It solved the immediate instability problem, but more importantly, it established the correct long-term pattern for any future feature that touches live services.<\/p>\n<h3>\ud83d\udeab Preventing Downtime<\/h3>\n<p>Another major focus was preventing bad configuration from affecting the active stack.<\/p>\n<p>That meant validating before reload, rolling back on failure, and treating config generation as something that could go wrong in very real ways rather than something to assume will always behave nicely.<\/p>\n<h3>\ud83c\udfd7\ufe0f Architecture Evolution<\/h3>\n<p>Phase 6 also sharpened the architecture itself.<\/p>\n<p>What started in earlier phases as sensible separation between controllers and services has now become a much more serious application design rule.<\/p>\n<p>The panel is no longer just storing records and rendering pages. It is controlling server behaviour.<\/p>\n<p>That raises the bar for how disciplined the code needs to be.<\/p>\n<hr \/>\n<h2>\ud83c\udfa8 UI Refinement<\/h2>\n<p>Although Phase 6 was primarily a backend and infrastructure phase, the panel continued evolving on the usability side as well.<\/p>\n<p>The interface remained consistent with the existing KR0311 panel structure:<\/p>\n<ul>\n<li><strong>@extends(&#8216;layouts.panel&#8217;)<\/strong><\/li>\n<li><strong>view(&#8216;dashboard.index&#8217;)<\/strong><\/li>\n<li><strong>.nav-link sidebar system<\/strong><\/li>\n<li><strong>public\/css\/panel.css<\/strong><\/li>\n<li><strong>no Tailwind<\/strong><\/li>\n<li><strong>no inline styles<\/strong><\/li>\n<\/ul>\n<p>This phase was not about redesigning the panel.<\/p>\n<p>It was about refining the flow around accounts, sites, and provisioning so the experience becomes more intuitive as the system grows.<\/p>\n<p>The structure continues moving in a more cPanel-style direction, with clearer separation between account-level ownership and site-level actions, cleaner navigation paths, and more obvious progression from data entry to real provisioning outcomes.<\/p>\n<p>Not flashy. Just increasingly usable, which is usually the better trade.<\/p>\n<hr \/>\n<h2>\ud83d\udd25 Why This Phase Matters<\/h2>\n<p>Phase 6 is a major milestone because it is the point where the control panel stopped being a structured admin app and started acting like an actual hosting platform.<\/p>\n<p>The system now controls the web stack directly.<\/p>\n<p>Sites are no longer just stored in the database.<\/p>\n<p>They are now capable of becoming live, server-backed web properties with Nginx and PHP-FPM provisioned through the panel itself.<\/p>\n<p>That is a substantial leap in capability.<\/p>\n<p>More importantly, it was achieved without throwing away the safety rules established in earlier phases.<\/p>\n<p>The result is not just more power, but more power handled properly.<\/p>\n<hr \/>\n<h2>\ud83d\udd1c What\u2019s Next in Phase 7<\/h2>\n<p>With web stack provisioning in place, the next logical step is to move into the domain readiness and certificate layer.<\/p>\n<p><strong>Phase 7 will build on this foundation with work around:<\/strong><\/p>\n<ul>\n<li><strong>SSL provisioning<\/strong><\/li>\n<li><strong>DNS-aware validation<\/strong><\/li>\n<li><strong>domain readiness checks<\/strong><\/li>\n<\/ul>\n<p>That will bring the panel closer to handling the full site activation lifecycle rather than just the local server-side portion of it.<\/p>\n<hr \/>\n<h2>\ud83c\udfc1 Final Thoughts<\/h2>\n<p>Phase 6 is one of those phases that looks tidy in a summary and felt considerably less tidy while building it.<\/p>\n<p>Under the surface, this was a meaningful shift in how the KR0311 Control Panel behaves.<\/p>\n<p>It now provisions the live web stack, handles validation and rollback properly, uses asynchronous jobs to avoid destabilising itself, and keeps the application layer clean while doing it.<\/p>\n<p>That is a big step forward.<\/p>\n<p>It also shows why the earlier phases mattered.<\/p>\n<p>The service boundaries, status model, logging strategy, and clean panel structure were not busywork.<\/p>\n<p>They were groundwork.<\/p>\n<p>Phase 6 is where that groundwork started paying rent.<\/p>\n<p>The panel is still early in the journey, but at this point it is no longer pretending to be a hosting control panel.<\/p>\n<p>It is becoming one.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Phase 6 introduces web stack provisioning, allowing the control panel to create Nginx vhosts, PHP-FPM pools, and live hosting configurations safely.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[34,20,50,53,56,55,38,49,52,54],"class_list":["post-53","post","type-post","status-publish","format-standard","hentry","category-control-panel","tag-control-panel-build","tag-custom-hosting-control-panel","tag-infrastructure-automation","tag-linux-hosting","tag-nginx-configuration","tag-php-fpm-pools","tag-self-hosted-control-panel","tag-server-automation","tag-server-provisioning","tag-web-stack-provisioning"],"_links":{"self":[{"href":"https:\/\/kr0311.com\/projects\/wp-json\/wp\/v2\/posts\/53","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kr0311.com\/projects\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/kr0311.com\/projects\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/kr0311.com\/projects\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kr0311.com\/projects\/wp-json\/wp\/v2\/comments?post=53"}],"version-history":[{"count":4,"href":"https:\/\/kr0311.com\/projects\/wp-json\/wp\/v2\/posts\/53\/revisions"}],"predecessor-version":[{"id":113,"href":"https:\/\/kr0311.com\/projects\/wp-json\/wp\/v2\/posts\/53\/revisions\/113"}],"wp:attachment":[{"href":"https:\/\/kr0311.com\/projects\/wp-json\/wp\/v2\/media?parent=53"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/kr0311.com\/projects\/wp-json\/wp\/v2\/categories?post=53"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/kr0311.com\/projects\/wp-json\/wp\/v2\/tags?post=53"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}