Deployment of Python Bot on Koyeb and Heroku using StayPresent
What Koyeb and Heroku Have in Common
Despite different dashboards and deployment flows, both platforms share the same core expectation that trips up most bot deployments: a web-facing process needs to bind to a port the platform assigns, typically via a PORT environment variable, and something needs to respond to health checks on that port. Neither platform inherently knows or cares what your bot is doing internally - Discord gateway connections, Telegram polling, whatever - it only cares whether the process it started is listening where expected.
Deploying to Heroku with a Procfile
Heroku uses a Procfile to declare how to start your app:
web: python main.py
main.py reads Heroku's assigned port the same way it would on Render or Railway:
import os
import staypresent
staypresent.web.json({"status": "running"})
staypresent.run(
"bot.py",
port=int(os.getenv("PORT", 8080)),
)
Heroku requires the process type to be web (not worker) for the platform to route external HTTP traffic to it, which matters if you're relying on Heroku's own health checks or want the app reachable at its public URL for staypresent.cron().
Heroku Dyno Sleeping
Free and low-cost Heroku dynos have historically been subject to inactivity sleeping, similar to Render's free tier. The fix is the same self-ping pattern:
staypresent.cron(
"https://my-app.herokuapp.com",
interval=240,
)
As with any platform, this must target the public herokuapp.com URL, never 127.0.0.1 or 0.0.0.0, since only external traffic resets an inactivity timer.
Deploying to Koyeb
Koyeb deploys directly from a Git repository or a Dockerfile, and - like Heroku and Render - injects a PORT environment variable that your service must bind to:
import os
import staypresent
staypresent.run(
"bot.py",
port=int(os.getenv("PORT", 8080)),
)
If you're deploying via Dockerfile on Koyeb, the same container setup described for VPS/Docker deployments applies unchanged - Koyeb just runs the container for you.
Koyeb Health Checks
Koyeb performs HTTP health checks against a configurable path. Point it at StayPresent's built-in /health route:
staypresent.web.json({"status": "running"})
# /health is provisioned automatically and returns {"status": "ok"}
This keeps Koyeb's platform-level health check independent from whatever custom status payload you're serving at /.
Shared StayPresent Configuration
Because the underlying requirement - a bound port, a health check, crash-resilient process management - is identical across Koyeb, Heroku, Render, and Railway, the exact same main.py typically works across all four with zero platform-specific branching:
import os
import staypresent
staypresent.web.json({"status": "running"})
staypresent.run(
"bot.py",
port=int(os.getenv("PORT", 8080)),
restart_on_crash=True,
max_restarts=5,
)
Full Example for Both Platforms
Heroku (Procfile):
web: python main.py
Koyeb (Dockerfile-based):
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]
Both point at the same entry point:
import os
import staypresent
staypresent.web.json({"status": "running"})
staypresent.cron(f"https://{os.getenv('APP_URL', '')}", interval=240)
staypresent.run(
"bot.py",
port=int(os.getenv("PORT", 8080)),
restart_on_crash=True,
max_restarts=5,
)
Best Practices
- Set the Heroku process type to
web, notworker, if you need the app reachable at a public URL for health checks or self-ping. - On Koyeb, configure the health check path explicitly to
/healthin the service settings rather than relying on the default root path. - Keep platform-specific values (like the public URL used for
cron()) in an environment variable rather than hardcoding them, since the URL differs between platforms and between staging/production.
Common Mistakes
- Declaring a
workerdyno on Heroku for a bot that also needs a keep-alive endpoint. Worker dynos aren't reachable via HTTP the way web dynos are, which defeats the purpose of running StayPresent's server at all. - Assuming Koyeb and Heroku behave identically for free-tier sleeping. Policies change over time on both platforms - check current documentation rather than assuming past behavior still applies.
- Not setting
APP_URLor equivalent, leavingcron()pointed at an empty string instead of the actual public domain.
FAQs
Can I use the exact same codebase across Render, Railway, Koyeb, and Heroku?
Yes - as long as you read PORT from the environment and target the correct public URL for self-ping, the StayPresent configuration doesn't need to change per platform.
Does Koyeb require Docker?
No, Koyeb also supports direct Git-based buildpack deployments, in which case you don't need a Dockerfile at all - just the same main.py entry point.
Do free tiers on these platforms still exist?
Availability and limits on free tiers change over time on every platform - always check current pricing pages before committing to a deployment plan.
Conclusion
A Koyeb Heroku Python bot deployment doesn't require separate code paths for each platform. Bind to the assigned PORT, expose StayPresent's /health endpoint for platform health checks, and optionally self-ping the public URL - the same main.py carries across Koyeb, Heroku, Render, and Railway without modification.
pip install staypresent[prod]
Comments
No comments yet. Start the discussion.