Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Maintenance Bundle Laravel Package

kilik/maintenance-bundle

A maintenance-mode bundle for PHP applications that makes it easy to enable maintenance mode, show a custom maintenance page, and let selected users or IP addresses bypass downtime during deployments. Lightweight and simple to configure for existing projects.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the honest framing: kilik/maintenance-bundle is a Symfony 2 bundle, not a Laravel package. There is no service provider, no config publish tag, no facade — dropping composer require kilik/maintenance-bundle into a Laravel app will install Symfony HttpKernel/EventDispatcher/DependencyInjection contracts and do nothing. The repository name (LexikFr/LexikMaintenanceBundle) also disagrees with the Composer name (kilik/maintenance-bundle), which is a fork-of-a-fork signal. Treat this as reference material and a feature spec, not a dependency.

Minimal steps if you just want the behaviour in Laravel:

  1. Don't install it. Laravel already ships the equivalent: php artisan down / php artisan up, backed by App\Http\Middleware\PreventRequestsDuringMaintenance and the storage/framework/down payload.
  2. Read three files in the Symfony repo to understand the design you're mirroring:
    • EventListener/MaintenanceListener.php — a kernel.request listener that asks a driver "are we locked?" and short-circuits with a 503 response.
    • Command/MaintenanceLockCommand.php / MaintenanceUnlockCommand.php — the two console commands the README brags about.
    • Driver/DriverInterface.php — isLocked() / lock() / unlock(), with file, Doctrine, and "service" implementations.
  3. First use case: replace the brittle "edit a middleware and deploy" pattern with a runtime toggle. In Laravel that's already php artisan down, so your first real task is usually extending it — an IP allowlist and a friendly 503 page.

Where to look first in your own app:

  • bootstrap/app.php (Laravel 11+) or app/Http/Kernel.php for where PreventRequestsDuringMaintenance sits in the global stack.
  • storage/framework/down — the JSON payload containing time, retry, status, secret, render, redirect. Everything the bundle does with a lock file, Laravel does with this file.
  • resources/views/errors/503.blade.php — your maintenance page, no config needed.

If you genuinely need the bundle (i.e. you maintain a legacy Symfony 2.8/3.x app), install with composer require kilik/maintenance-bundle, register new Lexik\Bundle\MaintenanceBundle\LexikMaintenanceBundle() in AppKernel.php, then bin/console lexik:maintenance:lock and bin/console lexik:maintenance:unlock. Verify the lock by hitting the site and confirming a 503 with Retry-After.

Implementation Patterns

1. Lock as a single, atomic piece of state

The bundle's core idea is worth copying: maintenance mode is data, not code. Never deploy a code change to enter maintenance — you'll deploy a second time to leave it, and the second deploy can fail.

// app/Console/Commands/MaintenanceDown.php — a Laravel take on lexik:maintenance:lock
class MaintenanceDown extends Command
{
    protected $signature = 'app:down {--ttl=0} {--allow=*} {--message=}';

    public function handle(): int
    {
        $payload = [
            'time'    => now()->getTimestamp(),
            'retry'   => (int) $this->option('ttl') ?: null,
            'status'  => 503,
            'message' => $this->option('message'),
            'allow'   => $this->option('allow'), // CIDR list, unlike the bundle
        ];

        File::put(storage_path('framework/down'), json_encode($payload));

        return self::SUCCESS;
    }
}

Keep up as the exact inverse (delete the file) and make both idempotent — lock on an already-locked site should succeed, not error, so your deploy scripts can call it unconditionally.

2. IP allowlist belongs in config, not in the lock file

The bundle puts authorized.ips in config.yml under lexik_maintenance.authorized. That's the right instinct: the allowlist is stable policy, the lock is transient state. In Laravel:

// config/maintenance.php
return [
    'allow' => array_filter(explode(',', env('MAINTENANCE_ALLOW', ''))),
    'bypass_secret' => env('MAINTENANCE_SECRET'),
    'retry_after'   => 3600,
];

Then a middleware in front of PreventRequestsDuringMaintenance:

class AllowlistedMaintenance extends PreventRequestsDuringMaintenance
{
    protected function inExceptArray($request): bool
    {
        foreach (config('maintenance.allow') as $cidr) {
            if (IpUtils::matches($request->ip(), $cidr)) {
                return true;
            }
        }
        return parent::inExceptArray($request);
    }
}

The bundle only does exact string matches on REMOTE_ADDR. If you're behind a load balancer you must trust proxies (TrustProxies) or every request looks like the LB's IP and the allowlist becomes "everyone or no one."

3. Shape the 503 response deliberately

The bundle returns 503 with Status: Service Temporarily Unavailable and Retry-After. Do the same — Retry-After is what stops Google from treating the page as a soft-404 and what stops naive uptime checkers from paging you at 3 a.m. every minute.

  • 503 (not 502/500) — it is semantically "temporarily unavailable."
  • Retry-After in seconds or HTTP-date.
  • Inline all CSS and images in errors/503.blade.php. Once the app is down, asset routes are down too, and you get an unstyled white page — the single most common complaint about this bundle and Laravel's default alike.

4. Command-driven ops in deployment pipelines

The bundle's whole surface is two commands. Mirror that discipline — a maintenance toggle should be invokable from CI, not from a web admin panel:

php artisan app:down --ttl=600 --allow=203.0.113.9 --message="Upgrading database"
php artisan migrate --force
php artisan app:up

Wrap it so a failed migration still unlocks (otherwise the previous deploy's traffic sees 503 forever):

php artisan app:down || exit 1
trap 'php artisan app:up' EXIT
php artisan migrate --force

5. Driver abstraction — only if you're multi-node

The bundle supports driver: file, driver: database, and driver: service. This exists because a lock file is per-server. If you have three app servers behind a load balancer, lexik:maintenance:lock locks exactly one of them and users see a coin flip.

In Laravel the same trap applies: storage/framework/down is local. On multi-node, either:

  • Run the toggle on every node (capistrano-style deploy hooks, or artisan down in each container's entrypoint), or
  • Replace the file check with a shared flag — a cache key (Cache::get('maintenance')) or a settings table row. This is where the bundle's database driver is genuinely instructive.

6. Exclude health checks and webhooks

kernel.request listeners and Laravel's global middleware both run on everything. Your ALB health check, your Kubernetes liveness probe, and your Stripe webhook endpoint will all get 503 and start pulling instances out of rotation or re-queuing events. Add them to the except list:

$middleware->preventRequestsDuringMaintenance(except: [
    'health', 'up', 'webhooks/*',
]);

Gotchas and Tips

Zero stars, zero dependents, last release 2022-01-28 — treat it as abandoned. No security response, no Symfony 6/7 support, no PHP 8.2+ guarantees. It exists as a historical artifact of Symfony 2's service-container era. Do not add it to a new project, in any framework.

Package/repo name mismatch. kilik/maintenance-bundle vs LexikFr/LexikMaintenanceBundle means the fork's composer.json may still declare the original name, or the branch aliasing is stale. If you must install it, check the actual name and require constraints in composer.json first — composer why-not will tell you fast.

ttl: 0 means "never expire." The bundle's TTL is a self-destruct for the lock, and the default is no expiry. This is a footgun: a deploy that dies between lock and unlock leaves the site down indefinitely. Always pass a finite TTL in CI (--ttl=1800), even if you intend to unlock manually.

Lock file in the cache directory is a classic mistake. Symfony's cache:clear wipes %kernel.cache_dir%. If the file driver's filename points inside the cache dir, deploying (which typically clears cache) silently un-locks the site mid-deploy. Put it in var/ or app/var/, never var/cache/.

Permissions. The console user writes the lock file; the web-server user reads it. A 0600 file written by your deploy user produces a "maintenance mode is off" result for PHP-FPM (unreadable → isLocked() returns false), or an exception. Check umask and the file's group.

CLI and web use the same APP_ENV. php artisan down in local doesn't stop a production container, and vice versa. In Symfony this manifests as lexik:maintenance:lock --env=prod. Be explicit in scripts.

The listener doesn't cover queue workers, cron, or artisan commands. HTTP middleware only guards HTTP. If your maintenance window is a schema migration, queued jobs will still run and crash against the new schema. Pause the queue explicitly (php artisan queue:pause on Horizon, or a Cache flag your jobs check) — the bundle gives you nothing here.

Sub-requests and ESI fragments re-enter kernel.request. If your maintenance listener has a low priority or doesn't guard against sub-requests, you can get a 503 body nested inside a 200 page. Laravel's equivalent: be careful with Http:: internal calls and with middleware that runs on both console and HTTP.

Extension points (Symfony): the service driver lets you implement DriverInterface against your own store — that's the intended hook for "maintenance flag lives in Redis/DB." The template config key overrides the 503 Twig template; the response key overrides status code and headers. In Laravel, the equivalent extension points are errors/503.blade.php, the --render option, --redirect, and a custom middleware extending PreventRequestsDuringMaintenance.

Debugging checklist when "the site says it's down but the file is gone":

  1. curl -I https://yourapp/ — read the Status: and Retry-After headers to confirm it's your maintenance page and not an upstream proxy's.
  2. Check the real path the app resolves (storage_path('framework/down') vs the container's working dir).
  3. Check opcache/realpath cache — a long-running FPM process can hold a stale view of a deleted file on some filesystems; opcache.validate_timestamps and revalidate_freq matter.
  4. Check TrustProxies — the allowlist failing is almost always $request->ip() returning the LB.

The one idea worth stealing permanently: a maintenance toggle must be a data flip that takes effect on the next request, requires no deploy, and is reversible by one command. Everything else in this bundle — the file/database/service drivers, the IP allowlist, the TTL, the Retry-After header — is a detail you can reconstruct in ~80 lines of Laravel middleware and two Artisan commands, with modern PHP support and no abandoned dependency.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib