How to Put a Self-Hosted App Into Maintenance Mode for Safe Updates and Migrations
Use maintenance mode, write freezes, worker drains, and verification checklists so risky upgrades and migrations do not leave your self-hosted app half-updated and half-writing to old assumptions.
How to pause writes, calm background activity, perform a risky change, and reopen traffic only after the stack proves it is consistent.
Schema migrations, storage moves, major app upgrades, and any change where stale writes would create messy rollback decisions.
The most common failure is believing the app is quiet while a worker, webhook, or queue consumer is still writing in the background.
Before you begin
- Know whether your app has a built-in maintenance mode, read-only mode, or queue pause feature.
- Know which components can still write outside the main web UI: workers, cron jobs, webhooks, sync jobs, and admin scripts.
- Have a fresh backup or snapshot path that matches the change you are about to make.
This guide covers a pattern many operators skip: making the app intentionally quiet before a risky change. Updating software is one thing. Updating software while writes continue against a changing schema, storage path, or queue contract is how subtle corruption and confusing rollback paths appear.
Step 1: Choose the right maintenance pattern
There are three common patterns:
- App-native maintenance mode: best when the application can serve a maintenance page while refusing writes internally.
- Reverse-proxy maintenance page: useful when the app has no built-in mode and you need to block user traffic at Nginx or another proxy.
- Read-only or drained background mode: useful when users may still view content, but writes and jobs must pause during migrations.
Pick the smallest pattern that creates a clear write boundary. Blocking all traffic is not always necessary, but pretending users can keep writing safely during a schema change usually is.
Step 2: Prepare the maintenance window before you announce it
Document the exact change, the rollback point, the affected services, and the reopen criteria. This is the difference between a controlled window and vague downtime.
# Example checklist items
- announce start time
- confirm backup finished and is readable
- confirm worker pause method
- confirm app health check command
- confirm rollback command or image tag
If the app has users, even a small one-user environment, decide what message they should see. A clean maintenance page is better than unexplained errors that look like data loss.
Step 3: Drain writes and background work
Before the risky change, quiet the system. That often means:
- pause queue workers or background job consumers
- disable cron tasks that would create writes during the window
- temporarily block external webhooks if they can enqueue work
- let in-flight requests finish before changing storage or schema
For Docker Compose stacks, this may mean scaling workers down, stopping specific worker services, or disabling scheduled jobs externally while the main app shows maintenance status.
# Example worker pause on a Compose stack
docker compose stop worker
# Example app or proxy verification
docker compose logs --tail=50 app
If you cannot fully stop writes, at least define which writes remain allowed and why. Unacknowledged background activity is where many rollback surprises come from.
Step 4: Perform the risky change
Once the app is quiet, do one class of work at a time. Pull images, apply migrations, switch volumes, or change proxy configuration. Resist the temptation to mix unrelated cleanup into the same window. A maintenance period is not the time to also rename services, tidy env files, or change logging drivers just because you are already in production.
Keep verification checkpoints between steps. After a schema migration, test connectivity before moving to the next action. After a storage move, confirm ownership and paths before restarting the app.
Step 5: Validate before reopening traffic
Do not reopen because the container is merely running. Validate the real paths that matter:
- the web app loads
- auth still works
- writes succeed where expected
- background workers reconnect cleanly
- logs stay free of migration or path errors
# Example checks
docker compose ps
docker compose logs --tail=100 app
docker compose logs --tail=100 worker
If the app supports a health endpoint or smoke test command, use it now. Reopening traffic without a smoke test is just a delayed discovery mechanism.
Step 6: Reopen in a controlled way
Re-enable the app in layers. Remove the maintenance page or app lock, then restart workers, then re-enable scheduled jobs and webhook delivery. Watch logs after each step. This staged reopen helps isolate failures to the last thing you changed instead of turning every post-maintenance issue into one large mystery.
Only after the system is stable should you close the window and mark the change complete.
Expected outcomes
- Users either see a clear maintenance state or a controlled read-only experience.
- Background jobs stop writing during the risky part of the change.
- The app reopens only after functional checks pass.
- Rollback remains realistic because the write boundary stayed clear.
Troubleshooting
Users can still write during maintenance: you blocked the web path but left another API, admin route, or worker alive. Check reverse-proxy exceptions and background services.
Workers replay stale jobs after reopen: the queue was paused late or not drained. Review job age and whether queued work assumed the old schema.
The app looks fine until users resume traffic: the reopen happened before real write tests. Put the app back into maintenance mode, inspect logs, and test one write path at a time.
Rollback feels unsafe: the app was never truly quiet. Restore the clearest known-good state, then re-run the window with a stricter write freeze.
What to do next
Maintenance mode becomes even more important before disaster-recovery work or major restores. Continue with How to Restore a Docker Compose App on a Fresh VPS After a Failure.
