6 min read

Putting your app in maintenance mode with Kamal

Sometimes you need to take down an app for a brief period of time for maintenance. Maybe you need to do a large database migration or maybe you need to release a breaking configuration change and don’t want to trigger a bunch of 500s to your users.

A new maintenance mode feature was just added to Kamal(still on main, no official versioned release yet) and it was perfect timing for an upgrade that I was about to rollout for an application.

When you put an application in maintenance mode you want to return a 503 with a simple “WE’RE DOING SOMETHING” (In Tim Robinson’s voice) page which is exactly what the new feature does, lets take a look.

Maintenance mode is actually more of a feature of kamal-proxy via the stop and resume commands but it's now usable from kamal. You’ll see that when you run kamal app maintenance it tells kamal-proxy to stop sending traffic to it which then starts returning kamal-proxy's default 503 page.

➜  phxbrief git:(main) ✗ kamal app maintenance
Running the pre-connect hook...
  INFO [ad831612] Running /usr/bin/env .kamal/hooks/pre-connect as n@localhost
  INFO [ad831612] Finished in 0.599 seconds with exit status 0 (successful).
  INFO [f26c0b5c] Running /usr/bin/env mkdir -p .kamal on x.x.x.x
  INFO [f26c0b5c] Finished in 1.909 seconds with exit status 0 (successful).
Acquiring the deploy lock...
  INFO [2f14be90] Running docker exec kamal-proxy kamal-proxy stop phxbrief-web-production --drain-timeout="30s" on x.x.x.x
  INFO [2f14be90] Finished in 0.450 seconds with exit status 0 (successful).
Releasing the deploy lock...

The important part from the maintenance command is this line:

docker exec kamal-proxy kamal-proxy stop phxbrief-web-production --drain-timeout="30s"

You can also see that it’s setting a drain-timeout when it starts to put your app into maintenance mode, this is how long kamal-proxy will allow in-flight requests to complete. You can customize this by setting drain_timeout within your config/deploy.yml if you need more or less time.

Once that finishes running you’ll now see the default 503 page which is served up via kamal-proxy, you can see the default proxy pages here.

That’s pretty neat. What’s even neater is you can customize the message to show on the 503 page to your users.

kamal app maintenance -d production --message "WE'RE DOING SOMETHING"

Pretty clean and simple. If you want to take this further and provide your own 503 page you just need to let Kamal know where your 503.html page lives. An obvious spot for a Rails app is to utilize the public directory where there’s already a 404.html, 422.html, and 500.html page with a default Rails app.

error_pages_path: public

If you don’t already have a public/503.html page, go ahead and create one. I’ll just create a simple one with a Copilot prompt:

Create a simple 503 maintenance page with https://cdn.simplecss.org/simple.min.css as the style and with a header of "Maintenance Mode" and a highlighted message below the header that says "We're upgrading a few things, we'll be back soon."

That generated this masterpiece.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>503 Service Unavailable</title>
  <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
</head>

<body>
  <header>
    <h1>Maintenance Mode</h1>
  </header>
  <main>
    <p style="background-color: #ffeb3b; padding: 10px; border-radius: 5px;">
      We're upgrading a few things, we'll be back soon.
    </p>
  </main>
</body>

</html>

With our new public/503.html page added we can test out our new maintenance page. Since this is a new file that doesn’t yet exist on our server we have to deploy our app first so that kamal-proxy can serve it, the maintenance command doesn’t do that automatically for us.

With error_pages_path: public in place when you deploy now you’ll see a few lines in your output where you can see Kamal is uploading your custom error pages. Kamal uploads all files within your error_pages_path that match 4xx.html and 5xx.html so that’ll automatically upload our new 503.html page as well as the default Rails pages we already have in there like 404.html.

  INFO [7ac4af96] Running /usr/bin/env mkdir -p .kamal/proxy/apps-config/phxbrief-production/error_pages on x.x.x.x
  INFO [7ac4af96] Finished in 0.149 seconds with exit status 0 (successful).
  INFO Uploading /var/folders/6q/53gfp0q92gngndncp5mmk9cr0000gn/T/kamal-error-pages20250510-88677-1k3n6k/5a60c4388cb62846a97bc3c164eead2090e6a5b1_uncommitted_c19d39eeec25e84f/422.html 100.0%
  INFO Uploading /var/folders/6q/53gfp0q92gngndncp5mmk9cr0000gn/T/kamal-error-pages20250510-88677-1k3n6k/5a60c4388cb62846a97bc3c164eead2090e6a5b1_uncommitted_c19d39eeec25e84f/500.html 100.0%
  INFO Uploading /var/folders/6q/53gfp0q92gngndncp5mmk9cr0000gn/T/kamal-error-pages20250510-88677-1k3n6k/5a60c4388cb62846a97bc3c164eead2090e6a5b1_uncommitted_c19d39eeec25e84f/404.html 100.0%
  INFO Uploading /var/folders/6q/53gfp0q92gngndncp5mmk9cr0000gn/T/kamal-error-pages20250510-88677-1k3n6k/5a60c4388cb62846a97bc3c164eead2090e6a5b1_uncommitted_c19d39eeec25e84f/503.html 100.0%

With our new 503.html page in place on the server we can now enable maintenance mode and check it out.

Not the greatest maintenance page but it’ll work. We can also override the maintenance mode message on our custom error page just like we did before by using the same template tags that the default 503.html page uses, here’s how the custom message is rendered in the default page.

{{ if .Message }}
<p>{{ .Message }}</p>
{{ else }}
<p><strong>The service is temporarily unavailable.</strong> It may be overloaded,<br> or undergoing scheduled maintenance. Please try again later.</p>
{{ end }}

Updating our new 503.html page with the custom message template tags then looks like this.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>503 Service Unavailable</title>
  <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
</head>

<body>
  <header>
    <h1>Maintenance Mode</h1>
  </header>
  <main>
    <p style="background-color: #ffeb3b; padding: 10px; border-radius: 5px;">
      {{ if .Message }}
      {{ .Message }}
      {{ else }}
      We're upgrading a few things, we'll be back soon.
      {{ end }}
    </p>
  </main>
</body>

</html>

Once you’re done with your maintenace you can easily put your app back online with the live command.

kamal app live
➜  phxbrief git:(main) ✗ be kamal app live -d production
Running the pre-connect hook...
  INFO [b342d62e] Running /usr/bin/env .kamal/hooks/pre-connect as n@localhost
  INFO [b342d62e] Finished in 0.495 seconds with exit status 0 (successful).
  INFO [19cfdf74] Running /usr/bin/env mkdir -p .kamal on x.x.x.x
  INFO [19cfdf74] Finished in 0.992 seconds with exit status 0 (successful).
Acquiring the deploy lock...
  INFO [99982ae9] Running docker exec kamal-proxy kamal-proxy resume phxbrief-web-production on x.x.x.x
  INFO [99982ae9] Finished in 0.294 seconds with exit status 0 (successful).
Releasing the deploy lock...

You can see that the important command for bringing your app back online is the kamal-proxy’s resume command.

docker exec kamal-proxy kamal-proxy resume phxbrief-web-production

The live and maintenance command only take a few seconds to run even with a long drain_timeout which makes it pretty straightforward to easily take things offline for as long as you need. These two new commands will be available with the next release of Kamal, you can also use these right now by pointing Kamal to basecamp/kamal in your Gemfile.

Maintenance mode by djmb · Pull Request #1497 · basecamp/kamal
Adds support for maintenance mode to Kamal. There are two new commands: kamal app maintenance - puts the app in maintenance mode kamal app live - puts the app back in live mode In maintenance mod…