Deploying to Render
Render is a popular platform for deploying web services.
What you typically need
Section titled “What you typically need”- a
requirements.txt - a WSGI entrypoint (
wsgi.py) - a start command (Gunicorn)
Typical start command
Section titled “Typical start command”gunicorn "wsgi:app" --bind 0.0.0.0:$PORTEnvironment variables
Section titled “Environment variables”Set in Render dashboard:
SECRET_KEYDATABASE_URL(if using managed DB)
Static files
Section titled “Static files”If your app serves lots of static assets, consider:
- platform static hosting
- or put a CDN in front
Deployment sanity checklist
Section titled “Deployment sanity checklist”- debug is off
- migrations run
- logs show requests responding
- health endpoint returns 200
What every host needs from you
Section titled “What every host needs from you”Platforms differ in their dashboards and almost not at all in what they require. Learn the four things below once, and each host becomes a matter of where you type them:
flowchart TD A["a WSGI entry point
module:variable, e.g. app:app"] --> D["the platform runs it"] B["a build command
usually pip install -r requirements.txt"] --> D C["a start command
gunicorn, bound to the platform's PORT"] --> D E["configuration from environment variables
never from a committed file"] --> D D --> L["a running service behind HTTPS"]
The two commands
Section titled “The two commands”pip install -r requirements.txtgunicorn app:app --bind 0.0.0.0:$PORT$PORT is the part people get wrong. The platform chooses a port and passes it in; an
app hard-coded to 8000 will not receive traffic and the deploy will be marked unhealthy
for a reason the logs make look unrelated.
app:app means “the variable app in app.py”. With an application factory it is
different:
gunicorn "myapp:create_app()" --bind 0.0.0.0:$PORTLocally, that target can be checked without any platform at all:
import importlib
mod, _, attr = "app:app".partition(":")
wsgi = getattr(importlib.import_module(mod), attr)
assert callable(wsgi) # a WSGI application is a callableMeasured on the demo app: the target resolved to a Flask object, callable was True,
and serving it returned 200. If that assertion fails locally, no amount of dashboard
configuration will help.
Configuration and the filesystem
Section titled “Configuration and the filesystem”Set SECRET_KEY, DATABASE_URL and anything else as environment variables in the
dashboard, not in a committed file. This is exactly the .env model with a different
place to type it.
Two constraints worth planning around on any platform-as-a-service:
- The filesystem is ephemeral. Anything written to disk disappears on the next deploy
or restart. A SQLite file in
instance/works until the first redeploy, then silently resets. Use a managed database, and object storage for uploads. - Free tiers sleep. An idle service is suspended and the next request pays the cold start — tens of seconds. Fine for a demo, not for anything with users waiting.
A checklist before you deploy anything
Section titled “A checklist before you deploy anything”| check | why |
|---|---|
requirements.txt pins exact versions | an unpinned dependency can change under you between deploys |
gunicorn is in it | it is a dependency of the deployment, not of your laptop |
the start command binds $PORT | a hard-coded port receives no traffic |
debug is off | the interactive debugger is remote code execution |
SECRET_KEY comes from the environment | a committed key is a forged session for anyone |
| the database is not SQLite on local disk | it disappears on redeploy |
See it move
Section titled “See it move”Check yourself
Section titled “Check yourself”-
Why must the start command bind to $PORT rather than a fixed port?
The confusing part is that gunicorn reports a successful start, so the logs look healthy while the platform cannot reach it.
pch.quizShowAnswer
B — the platform assigns the port and connects its health check to it; a hard-coded port receives no traffic — The confusing part is that gunicorn reports a successful start, so the logs look healthy while the platform cannot reach it.
-
Why is a SQLite file in instance/ a poor choice on a platform-as-a-service?
It works right up until the first redeploy, then silently resets. Use a managed database, and object storage for uploads.
pch.quizShowAnswer
B — the filesystem is ephemeral, so the database disappears on the next deploy or restart — It works right up until the first redeploy, then silently resets. Use a managed database, and object storage for uploads.
-
With an application factory, what should the gunicorn target be?
gunicorn accepts a call expression, so the factory runs and returns the app. Verify the target locally: import the module, get the attribute and assert it is callable.
pch.quizShowAnswer
B — myapp:create_app() — gunicorn accepts a call expression, so the factory runs and returns the app. Verify the target locally: import the module, get the attribute and assert it is callable.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading