Skip to content

Added a static nginx maintenance page switched on by 'VORTEX_NGINX_MAINTENANCE_MODE'. - #3225

Merged
AlexSkrypnyk merged 8 commits into
mainfrom
feature/nginx-maintenance
Oct 21, 2026
Merged

AlexSkrypnyk merged 8 commits into
mainfrom
feature/nginx-maintenance

Conversation

@AlexSkrypnyk

@AlexSkrypnyk AlexSkrypnyk commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Summary

Setting VORTEX_NGINX_MAINTENANCE_MODE=1 on an environment now makes the nginx container answer every request with HTTP 503 and the static page in .docker/config/nginx/maintenance.html. The check lives in /etc/nginx/conf.d/drupal/server_prepend-maintenance.conf and runs in nginx's server rewrite phase, so it fires before the redirects map, the index.php rewrite and every location, and no request reaches PHP-FPM.

Until now the only way to keep visitors out was Drupal's own maintenance mode (drush maint:set 1, or VORTEX_PROVISION_USE_MAINTENANCE_MODE during provisioning), which runs in PHP and needs a working codebase and database. That's no help when Drupal itself has to stay down: a database migration on Lagoon, a release that broke bootstrap, or an incident where nobody should touch the site.

With the variable unset, which is the default, nginx serves the site exactly as before. On Lagoon the switch is a runtime variable plus a redeploy, because the image entrypoint writes the value into the config when the container starts, and locally it's VORTEX_NGINX_MAINTENANCE_MODE=1 ahoy up nginx. Drupal's maintenance mode, Lagoon's pod probes on port 50000 (/nginx_status) and the installer prompts are untouched.

Before / After

BEFORE

  request ──▶ nginx ──▶ location / ──▶ @drupal ──▶ PHP-FPM ──▶ Drupal
                                                   (needs code and a DB)

AFTER (VORTEX_NGINX_MAINTENANCE_MODE=1 on the nginx container)

  request ──▶ nginx
               │
               ├─ server_prepend-maintenance.conf: return 503
               │    (server rewrite phase, before any location)
               ▼
             error_page 503 ──▶ location = /__maintenance.html (internal)
                                  │
                                  ▼
                                /etc/nginx/html/maintenance.html
                                HTTP 503 for every path and method

             PHP-FPM: never contacted
             :50000/nginx_status (Lagoon probes): unaffected

AFTER (variable unset, or any value other than "1"): same as BEFORE

Changes

nginx config and page

  • .docker/config/nginx/maintenance.conf (new): set $vortex_maintenance "${VORTEX_NGINX_MAINTENANCE_MODE:-0}", which the Lagoon entrypoint's envplate pass fills in at container start, then a server-level if that returns 503 when the value is exactly 1, and error_page 503 /__maintenance.html served from an internal exact-match location. The page URI is exempt from the check because nginx runs the server rewrite phase again for the error-page redirect.
  • The error page is a URI rather than a named location on purpose: nginx switches the method to GET for a URI error page, so POST, PUT, DELETE and OPTIONS get the 503 page instead of a 405 Not Allowed from the static file handler.
  • .docker/config/nginx/maintenance.html (new): a self-contained page with inline CSS, light and dark color schemes, and the installer's YOURSITE token, so scaffolded sites show their own name.
  • .docker/nginx-drupal.dockerfile: copies the config to /etc/nginx/conf.d/drupal/server_prepend-maintenance.conf and the page to /etc/nginx/html/maintenance.html. The entrypoint's envplate pass doesn't touch that directory, so ${...} in a customized page survives.

Local preview

  • docker-compose.yml: passes VORTEX_NGINX_MAINTENANCE_MODE to the nginx service only. In the shared x-environment anchor it would change the cli and php config too, and ahoy up nginx would recreate them as well.

Tests

  • SubtestAhoyTrait::subtestAhoyNginxMaintenance(), called from AhoyWorkflowTest::testAhoyWorkflowStateless() right after the Fast 404 subtest. It checks the homepage is Drupal, recreates nginx with the variable, runs nginx -t, and asserts 503 plus the page for /, a form POST and a core static file. It then stops php and asserts the page still comes back, and finally recreates nginx without the variable and asserts Drupal is back, since the later doctor subtest only accepts 200 or 403.
  • SutTrait::assertVortexFilesPresent(): asserts both new files ship with every installation.
  • Regenerated the 4 docker-compose JSON fixtures and the installer fixtures: the baseline, plus hunk shifts in the scenarios that diff docker-compose.yml, nginx-drupal.dockerfile or the Lagoon block of docs/deployment.md.
  • The edge cases the workflow test doesn't reach were checked against uselagoon/nginx-drupal:26.9.0 directly: HEAD, PUT, DELETE and OPTIONS, an empty value and true, Lagoon basic auth, the PHP-routed /.lagoonhealthz (NGINX_FASTCGI_PASS=127.0.0.1), conditional requests against the page's ETag, and the probe port.

Docs

  • hosting/lagoon.mdx: new "Serve a maintenance page" section under routine operations, covering turning it on and off with the Lagoon CLI, confirming with curl, what it covers, and how to customize and preview the page.
  • development/provision.mdx: 1 paragraph telling Drupal's maintenance mode and the nginx page apart, with a link.
  • development/environment/docker.mdx: notes the nginx pass-through next to the anchor's variable list.
  • lagoon.variables.sh stub plus the regenerated variables.mdx row, and lagoonhealthz added to cspell.json.
  • Root docs/deployment.md (Lagoon block): a short "Maintenance page" note that points at the docs.

Decisions worth a look

  • Only 1 switches it on. Unset, empty, 0 and true all leave the site online, in line with the other Vortex flags.
  • Every request is covered, including /.lagoonhealthz. On Lagoon that endpoint goes to PHP, while the readiness and liveness probes hit /nginx_status on port 50000, which a server_prepend include doesn't touch.
  • No allowlist or bypass. Letting some clients through means reaching PHP, plus a real-IP design behind Lagoon's router and any CDN. Happy to add it as a follow-up if it's wanted.
  • No Retry-After or Cache-Control headers. Any add_header in the page location would drop the inherited X-LAGOON header, and browsers, Varnish, Fastly and Cloudflare don't cache a 503 by default. Projects can add them to their copy of the file.
  • Ships with every hosting choice, with no installer token. The nginx container runs locally for all of them and the config is inert until switched on, so local preview and other container hosting get it too. The how-to lives on the Lagoon page.
  • nginx-level auth still comes first. With BASIC_AUTH_USERNAME and BASIC_AUTH_PASSWORD set, visitors get the 401 prompt and then the page. Shield runs in Drupal, so the page is public on Shield-protected environments.

Screenshots

The page as a scaffolded site renders it, with YOURSITE replaced by the project name.

Maintenance page, light color scheme

Maintenance page, dark color scheme

Summary by CodeRabbit

  • New Features

    • Added a static Nginx maintenance mode that returns HTTP 503 responses before requests reach PHP.
    • Added a responsive maintenance page with light and dark theme support.
    • Added configuration support for enabling maintenance mode locally or through Lagoon.
  • Documentation

    • Documented activation, deployment, health checks, and disabling maintenance mode.
  • Tests

    • Added coverage verifying maintenance responses remain available while PHP is stopped and normal responses resume afterward.

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: ASSERTIVE
  • Plan: Team
  • Run ID: e7f04269-65f6-43c6-a258-aad4c43a6281
📥 Commits

Reviewing files that changed from the base of the PR and between 1b86151 and c24b809.

⛔ Files ignored due to path filters: 33
  • .vortex/installer/tests/Fixtures/handler_process/_baseline/.docker/config/nginx/maintenance.conf is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/_baseline/.docker/config/nginx/maintenance.html is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/_baseline/.docker/nginx-drupal.dockerfile is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/_baseline/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/custom_modules_search_without_solr/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/hosting_lagoon/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/hosting_lagoon/docs/deployment.md is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/hosting_project_name___lagoon/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/hosting_project_name___lagoon/docs/deployment.md is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/migration_disabled_lagoon/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/migration_disabled_lagoon/docs/deployment.md is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/migration_enabled/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/migration_enabled_circleci/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/migration_enabled_lagoon/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/migration_enabled_lagoon/docs/deployment.md is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/migration_fetch_source_acquia/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/migration_fetch_source_container_registry/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/migration_fetch_source_ftp/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/migration_fetch_source_lagoon/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/migration_fetch_source_s3/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/migration_fetch_source_url/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/modules_no_dev_modules_storybook/.docker/nginx-drupal.dockerfile is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/names/.docker/config/nginx/maintenance.html is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/non_interactive_config_file/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/non_interactive_config_string/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/provision_database_lagoon/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/provision_database_lagoon/docs/deployment.md is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/services_no_clamav/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/services_no_redis/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/services_no_solr/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/services_none/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/storybook_enabled/.docker/nginx-drupal.dockerfile is excluded by !.vortex/installer/tests/Fixtures/**
  • .vortex/installer/tests/Fixtures/handler_process/tools_no_dclint_migration/docker-compose.yml is excluded by !.vortex/installer/tests/Fixtures/**
📒 Files selected for processing: 18
  • .docker/config/nginx/maintenance.conf
  • .docker/config/nginx/maintenance.html
  • .docker/nginx-drupal.dockerfile
  • .vortex/docs/.utils/variables/extra/lagoon.variables.sh
  • .vortex/docs/content/development/environment/docker.mdx
  • .vortex/docs/content/development/provision.mdx
  • .vortex/docs/content/development/variables.mdx
  • .vortex/docs/content/hosting/lagoon.mdx
  • .vortex/docs/cspell.json
  • .vortex/tests/phpunit/Fixtures/docker-compose.env.json
  • .vortex/tests/phpunit/Fixtures/docker-compose.env_local.json
  • .vortex/tests/phpunit/Fixtures/docker-compose.env_mod.json
  • .vortex/tests/phpunit/Fixtures/docker-compose.noenv.json
  • .vortex/tests/phpunit/Functional/AhoyWorkflowTest.php
  • .vortex/tests/phpunit/Traits/Subtests/SubtestAhoyTrait.php
  • .vortex/tests/phpunit/Traits/SutTrait.php
  • docker-compose.yml
  • docs/deployment.md

Included review availability: This review used your included allowance. 1 included review remains after this review. Your included PR review attempts over the past 7 days set your current allowance at 3 reviews per hour.

Walkthrough

This change adds an Nginx static maintenance page that returns HTTP 503 when VORTEX_NGINX_MAINTENANCE_MODE is set to 1. It wires the setting into local and Lagoon environments, documents its use, and adds integration tests for enabled and disabled behavior.

Changes

Static maintenance mode

Layer / File(s) Summary
Nginx response and page
.docker/config/nginx/maintenance.conf, .docker/config/nginx/maintenance.html, .docker/nginx-drupal.dockerfile
Nginx returns HTTP 503 and serves the static maintenance page when the setting is 1. The Docker image installs the configuration and page.
Setting and deployment guidance
.vortex/docs/.utils/variables/extra/lagoon.variables.sh, docker-compose.yml, .vortex/tests/phpunit/Fixtures/*, .vortex/docs/content/development/*, .vortex/docs/content/hosting/lagoon.mdx, .vortex/docs/cspell.json, docs/deployment.md
The configuration passes the setting to Nginx. Documentation describes enabling, verifying, disabling, and previewing maintenance mode.
Integration coverage
.vortex/tests/phpunit/Functional/AhoyWorkflowTest.php, .vortex/tests/phpunit/Traits/Subtests/SubtestAhoyTrait.php, .vortex/tests/phpunit/Traits/SutTrait.php
The stateless Ahoy workflow checks HTTP 503 responses with PHP running or stopped, then checks application responses after maintenance mode is disabled. The file-presence check includes the Nginx configuration and HTML page.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Nginx
  participant MaintenanceHTML
  participant PHP
  Client->>Nginx: Send web request
  alt Maintenance mode is 1
    Nginx->>MaintenanceHTML: Resolve internal maintenance URI
    Nginx-->>Client: Return HTTP 503 and maintenance page
  else Maintenance mode is disabled
    Nginx->>PHP: Forward request
    PHP-->>Nginx: Return application response
    Nginx-->>Client: Return response
  end
Loading
Merge Risk: ⚪ Minimal · up to c0fed

Merge Risk: ⚪ Minimal · up to c0fed

The maintenance setting is wired into the nginx image and local configuration as intended. No issue identified here prevents merging after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 4 files. (14 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: a static Nginx maintenance page controlled by VORTEX_NGINX_MAINTENANCE_MODE.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 4 files. (14 skipped: 14 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

✨ Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

A rabbit checks the server light,
Nginx serves a page of white.
The waiting requests pause at the door,
While PHP is asked no more.
When maintenance signs depart,
Web replies resume their part.

Comment @coderabbitai help to get the list of available commands.

@github-actions

This comment has been minimized.

@AlexSkrypnyk

This comment has been minimized.

@github-actions

This comment has been minimized.

1 similar comment
@github-actions

This comment has been minimized.

@AlexSkrypnyk

This comment has been minimized.

1 similar comment
@AlexSkrypnyk

This comment has been minimized.

@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.09%. Comparing base (1b86151) to head (c0fed92).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3225      +/-   ##
==========================================
- Coverage   92.29%   92.09%   -0.20%     
==========================================
  Files         113      105       -8     
  Lines        5099     4933     -166     
  Branches       49        3      -46     
==========================================
- Hits         4706     4543     -163     
+ Misses        393      390       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@AlexSkrypnyk AlexSkrypnyk added this to the 1.42.0 milestone Oct 21, 2026
@github-actions

This comment has been minimized.

@AlexSkrypnyk

This comment has been minimized.

@github-actions

This comment has been minimized.

@AlexSkrypnyk

This comment has been minimized.

@github-actions

Copy link
Copy Markdown

Code coverage (threshold: 90%)

  Classes: 100.00% (1/1)
  Methods: 100.00% (2/2)
  Lines:   100.00% (233/233)
Per-class coverage
Drupal\ys_demo\Plugin\Block\CounterBlock
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% ( 10/ 10)

@AlexSkrypnyk

Copy link
Copy Markdown
Member Author

Code coverage (threshold: 90%)

  Classes: 100.00% (1/1)
  Methods: 100.00% (2/2)
  Lines:   100.00% (233/233)
Per-class coverage
Drupal\ys_demo\Plugin\Block\CounterBlock
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% ( 10/ 10)

@github-actions

Copy link
Copy Markdown

📖 Documentation preview for this pull request has been deployed to Netlify:

https://6ad89be9fecbe803db122e2f--vortex-docs.netlify.app

This preview is rebuilt on every commit and is not the production documentation site.

@AlexSkrypnyk AlexSkrypnyk added the Needs review Pull request needs a review from assigned developers label Oct 21, 2026
@AlexSkrypnyk
AlexSkrypnyk merged commit d51c741 into main Oct 21, 2026
@AlexSkrypnyk
AlexSkrypnyk deleted the feature/nginx-maintenance branch October 21, 2026 11:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs review Pull request needs a review from assigned developers

Projects

Status: Released in 1.42.0

Development

Successfully merging this pull request may close these issues.

1 participant