DEV Community

John Wick
John Wick

Posted on

Running a Bot Without a Web Server (and Vice Versa) in StayPresent

How StayPresent's web_server=False and bot-less run() modes work, plus the new exclude parameter for locking down static file exposure.

Running a Bot Without a Web Server (and Vice Versa) in StayPresent

Until v1.6.0, staypresent.run() always did both things at once: launch a bot, and run an HTTP server alongside it. That coupling made sense for the platform-hosting use case it was originally built for, but it didn't fit every deployment. Two new modes decouple these entirely, and a new exclude parameter tightens up a real security gap in static file serving. Here's how all three work.

Table of Contents

  1. Why Bot and Server Were Always Coupled Before
  2. Bot Supervision With No HTTP Server
  3. HTTP Server With No Bot at All
  4. When Each Mode Actually Makes Sense
  5. The exclude Parameter for Static Routes
  6. How Exclusions Interact Across Multiple Routes
  7. Full Examples
  8. Best Practices
  9. Common Mistakes
  10. FAQs
  11. Conclusion

Why Bot and Server Were Always Coupled Before

StayPresent's original purpose was solving one specific problem: a bot that needs an HTTP port purely to satisfy a hosting platform's health check. Since that's the overwhelmingly common case, run() handled both concerns together by default. But not every deployment needs that pairing — sometimes you want process supervision with zero HTTP surface, or an HTTP status service with nothing running underneath it at all.

Bot Supervision With No HTTP Server

import staypresent

staypresent.run("bot.py", web_server=False)
Enter fullscreen mode Exit fullscreen mode

This gives you StayPresent's full crash recovery and restart management — max_restarts, restart_delay, restart_reset_after, and (as of v1.6.0) heartbeat_timeout for hang detection — with zero HTTP server running at all. This is the right choice for a deployment where nothing external ever needs to reach an HTTP port: a background worker on infrastructure that doesn't impose a port-binding health check requirement, or a process where you're deliberately avoiding any network-facing surface for security reasons.

HTTP Server With No Bot at All

import staypresent

staypresent.run()
Enter fullscreen mode Exit fullscreen mode

Called with no bot configuration at all, run() now spins up only the web server. This is genuinely new capability — previously, StayPresent's HTTP server only existed as a side effect of supervising a bot. Now it works as a standalone deployment on its own, which is specifically useful for a dedicated status/health-check service — for example, a central /status dashboard aggregating information that other, separate StayPresent-managed bot deployments report into, or simply a lightweight always-on health endpoint with nothing else attached to it.

When Each Mode Actually Makes Sense

web_server=False fits a bot running somewhere that genuinely doesn't need an HTTP port — a Docker container on a VPS with no platform-level health check, or a script you're intentionally keeping off the network entirely.

No bot configured (server-only) fits a dedicated monitoring/status deployment, or a lightweight service that's purely about serving web.json()/web.html()/web.markdown() content — a documentation page, a public API stub, a webhook receiver — with no subprocess supervision involved at all.

The default (both together) remains the right choice for the vast majority of bot deployments on Render, Railway, Koyeb, or Heroku, where the HTTP port requirement is the whole reason StayPresent exists in the first place.

The exclude Parameter for Static Routes

Earlier versions of web.html()/web.markdown() served every file in the target directory as a static asset, not just the ones actually referenced from the page — a deliberate tradeoff for zero-config convenience, but one with a real security cost if a .env or source file happened to live in that same directory. exclude closes that gap directly:

staypresent.web.html(
    "templates/index.html",
    exclude=[".env", ".git", "*.py", "secrets.json"],
)
Enter fullscreen mode Exit fullscreen mode

Exact filenames, extensions, and glob patterns are all supported. A request for any excluded file now returns a clean 404, rather than the file's actual contents.

How Exclusions Interact Across Multiple Routes

If more than one registered route serves files out of the same physical directory, exclusions set on any one of them are now merged globally across all of them — closing a gap where a secondary route pointed at the same directory could previously be used to bypass another route's own exclusion list.

staypresent.web.html("shared/index.html", exclude=[".env"])
staypresent.web.markdown("shared/CHANGELOG.md", path="/changelog")
# .env is now excluded for BOTH routes, since they share a directory
Enter fullscreen mode Exit fullscreen mode

Full Examples

Bot-only, no HTTP server, on a VPS:

import staypresent

staypresent.run(
    "worker.py",
    web_server=False,
    restart_on_crash=True,
    max_restarts=5,
    heartbeat_timeout=30,
)
Enter fullscreen mode Exit fullscreen mode

Server-only, dedicated status deployment:

import staypresent

staypresent.web.status(title="Fleet Status")
staypresent.run(port=8080)
Enter fullscreen mode Exit fullscreen mode

Locked-down static serving:

import staypresent

staypresent.web.html(
    "public/dashboard.html",
    exclude=[".env", ".git", "*.py", "*.log"],
)
staypresent.run("bot.py")
Enter fullscreen mode Exit fullscreen mode

Best Practices

  • Use exclude on every web.html()/web.markdown() call whose target directory isn't 100% dedicated to public-facing files — it costs one line and closes a real exposure gap.
  • Reach for web_server=False specifically when you know for certain nothing external needs to health-check this deployment — don't default to it on a PaaS platform that actually requires the port.
  • Use server-only mode (run() with no bot) for status aggregation, not as a general-purpose web framework replacement — it's still built around the same web.* primitives, not full request routing.

Common Mistakes

  • Setting web_server=False on a platform that actually requires an HTTP port, which reintroduces the exact "platform marks deployment unhealthy" problem StayPresent exists to solve in the first place.
  • Assuming exclude retroactively protects a directory you've already deployed without it. Add exclusions before the first deploy where sensitive files might sit alongside a served template, not after.
  • Forgetting that exclusions merge across routes sharing a directory, and being surprised when a file is blocked on a route where you didn't explicitly list it — this is the intended, safer behavior as of v1.6.0.

FAQs

Does web_server=False still give me hang detection?
Yes — heartbeat_timeout and hang detection are independent of whether an HTTP server is running; they only depend on the bot's own heartbeat() calls.

Can I combine server-only mode with web.status()?
Yes — this is one of the more natural use cases for server-only mode: a dedicated status dashboard with no bot of its own.

Does exclude support regex, or only glob patterns?
Glob-style patterns (*.py, secrets.*) and exact filenames — not full regex.

Conclusion

Decoupling the bot and the web server gives StayPresent real flexibility beyond its original single use case, while exclude addresses a static-file exposure concern directly rather than leaving it as something you have to solve through directory hygiene alone. Together, they make v1.6.0 a meaningfully more general-purpose tool than earlier releases, without changing anything about how the default, most common setup behaves.

pip install "staypresent[prod]"
Enter fullscreen mode Exit fullscreen mode

Top comments (0)