A maintenance page that returns 200 tells search engines your entire site is now a page saying "back soon", and they will index it. The correct answer is 503 with a Retry-After header, which means temporarily unavailable, come back later - and costs nothing.

Nginx

server {
    # ...
    if (-f /var/www/site/maintenance.on) {
        return 503;
    }

    error_page 503 @maintenance;
    location @maintenance {
        root /var/www/site/public;
        rewrite ^ /maintenance.html break;
        add_header Retry-After 3600 always;
    }
}
sudo touch /var/www/site/maintenance.on     # on
sudo rm /var/www/site/maintenance.on        # off

A file switch means no reload, no configuration change, and it can be flipped from a deploy script.

Let yourself in

set $maint 0;
if (-f /var/www/site/maintenance.on) { set $maint 1; }
if ($remote_addr = 203.0.113.42)   { set $maint 0; }
if ($maint = 1) { return 503; }
Without an exemption you cannot test the fix you are deploying, and the usual response is to turn maintenance mode off while the site is still broken. Put your own address in before you need it.

What the page should say

  • That it is planned, and when it will be back - an actual time, not "shortly".
  • A way to reach you that does not depend on the site.
  • No dependency on the application: a static file with inline CSS.

Check it

curl -sSI https://yourdomain.com/ | grep -E '^HTTP|retry-after'
# HTTP/2 503
# retry-after: 3600
For a deploy that takes seconds, do not use maintenance mode at all: build into a new directory and switch a symlink. Nobody sees anything - see deploying with an atomic switch.