How to Host a Python Web App for Free (Render, Railway, Fly.io, PythonAnywhere)
To host a Python web app for free in 2026 you have four realistic options: Render's Free instance type, Railway's Free plan, PythonAnywhere's Beginner account and Fly.io's trial, plus a small VPS as the cheap-but-not-free fifth. None is free without conditions. Render sleeps your app after 15 idle minutes and caps you at 750 instance hours a month; Railway gives a one-time $5 trial and then $1 of usage a month; PythonAnywhere runs one app that you must renew every three months; Fly.io offers only a short trial before a credit card is required. These facts come from each provider's pricing or docs pages as of September 2026; check them again before you commit.
Below: the honest comparison, a complete Flask-on-Render deploy from GitHub, what changes between your laptop and a server, and the failures behind most broken first deploys.
What "free" means on each host (as of September 2026)
Render. The Free instance type gives a web service 0.1 CPU and 512 MB RAM. It spins down after 15 minutes without inbound traffic and takes about a minute to spin back up on the next request. You get 750 Free instance hours per workspace per calendar month; when they run out, Render suspends your free services until the next month. There is no persistent disk (files written at runtime vanish on redeploy), only a single instance, and outbound SMTP is blocked. No payment method is needed to create a free service. 750 hours is just over a 31-day month (744), so one always-on free service fits; two share the pool.
Railway. No open-ended free tier any more. New accounts get a one-time $5 trial credit that expires in 30 days, no credit card required, limited to 1 GB RAM and shared vCPU. After that the account reverts to the Free plan: $0 a month with $1 of usage credit and 0.5 GB RAM per service. The Hobby plan is $5 a month including $5 of usage and requires a card. A hobby project that runs all month will exceed $1 of usage.
Fly.io. No permanent free tier. The trial is 2 hours of machine runtime or 7 days, whichever comes first; after that a credit card is required. Pricing is pay as you go; the smallest machine (shared-cpu-1x, 256 MB) works out to roughly $2 a month running continuously, and machines can auto-stop when idle so you pay only for storage while stopped. Good value, not free.
PythonAnywhere. The Beginner account is genuinely free with no card: one web app at your-username.pythonanywhere.com, 512 MB of disk, and outbound HTTP(S) only to a whitelist of sites. Free web apps run for three months, then you must log in and click "Run until 3 months from today" or they are disabled. There is no cold start because the app is always running, which suits a college project that must stay reachable. It is a managed host with its own WSGI configuration page, so the git-push workflow below does not apply.
A VPS. DigitalOcean, Hetzner, Linode and others rent a small Linux machine for a monthly fee. Nothing is free, but nothing sleeps. You get root and every responsibility with it: Python, gunicorn under systemd, nginx, HTTPS, security updates. Worth doing once; not the place to start.
PyRun is not on this list on purpose. PyRun's /build and /site tools publish static HTML, CSS and JavaScript sites. They do not run a Python process, so a Flask or FastAPI backend cannot be hosted there. Build and test the app in the lessons, then deploy it to one of the hosts above.
Deploy a Flask app to Render from GitHub
The complete path for a Flask app; the FastAPI differences (one dependency, one start command) are noted inline. If you have not written the app yet, the first Flask web server lesson and the REST APIs with FastAPI lesson get you to a working local server first.
1. The app
# app.py
import os
from flask import Flask
app = Flask(__name__)
@app.get("/")
def home():
return {"message": "Hello from Render"}
@app.get("/health")
def health():
return {"status": "ok"}
if __name__ == "__main__":
port = int(os.environ.get("PORT", "5000"))
app.run(host="0.0.0.0", port=port)
The if __name__ == "__main__" block only runs when you start the file directly with python app.py during development. In production gunicorn imports app and serves it itself, so this block never executes there.
2. requirements.txt
Render runs pip install -r requirements.txt. Pin versions so the build that worked today also works in three months.
flask==3.1.1
gunicorn==23.0.0
For FastAPI the two lines are fastapi and uvicorn. To get exact pins, install into a virtual environment, confirm the app runs, then pip freeze > requirements.txt.
3. .gitignore
.venv/
__pycache__/
*.pyc
.env
The .env line matters most. Secrets go into Render's environment variables, never into git.
4. Push to GitHub
git init
git add app.py requirements.txt .gitignore
git commit -m "Flask app ready for Render"
git remote add origin https://github.com/<you>/<repo>.git
git push -u origin main
The git essentials lesson covers exactly these commands.
5. Create the web service on Render
In the Render dashboard choose New, then Web Service, and connect the GitHub repository. Fill in:
- Runtime: Python 3
- Build command:
pip install -r requirements.txt - Start command:
gunicorn app:app --bind 0.0.0.0:$PORT - Instance type: Free
For FastAPI the start command is the one Render's own docs give: uvicorn main:app --host 0.0.0.0 --port $PORT (with main.py holding app).
app:app means "the object called app inside the module app.py". If your file is server.py, write server:app. $PORT is filled in by Render at start time; its default is 10000, but never hard-code that.
Under Advanced, set the health check path to /health; Render polls it to decide a deploy is live. Add any secrets as environment variables on the same screen.
Click Create Web Service. The first build installs dependencies and takes a minute or two, then Render gives you an https://<name>.onrender.com URL. Every later git push to main redeploys automatically.
What changes between local and hosted
Four things differ, and each is a classic first-deploy failure if you miss it.
Host 0.0.0.0, not 127.0.0.1. Locally Flask's default of 127.0.0.1 is fine because you and the server are the same machine. On a host the request arrives from Render's proxy, and a server bound to 127.0.0.1 is invisible to it. Every Render web service must bind to 0.0.0.0.
PORT comes from the environment. You do not choose the port; the platform does. Read it with os.environ and fall back to a local default.
import os
port = int(os.environ.get("PORT", "5000"))
debug = os.environ.get("FLASK_DEBUG") == "1"
print(f"listening on 0.0.0.0:{port} debug={debug}")
# listening on 0.0.0.0:5000 debug=False
No debug mode. app.run(debug=True) gives you an in-browser debugger that can execute arbitrary Python. On a public URL that is a remote shell for anyone who finds it. In production you are not calling app.run at all; gunicorn or uvicorn serves the app.
Secrets via environment variables. Database URLs, API keys and SECRET_KEY are read with os.environ["SECRET_KEY"] and set in the Render dashboard. Locally, put them in a git-ignored .env file. The moment a key is committed, treat it as leaked and rotate it.
Pre-deploy checklist
- Start command or Procfile. Render reads the start command from the dashboard; Railway and Heroku-style hosts read a
Procfilecontainingweb: gunicorn app:app --bind 0.0.0.0:$PORT. Having both is harmless. - Pinned
requirements.txtthat includes the server (gunicornoruvicorn), not just the framework. A missing server is the usual reason a start command fails with "command not found". .gitignoreexcludes.venv/,__pycache__/and.env.- A
/healthroute that returns 200 quickly and touches nothing external. - Logging to stdout. Print statements and
loggingoutput go to the Render log viewer. Do not write log files; the disk is ephemeral. - Run the exact start command locally once:
gunicorn app:app --bind 0.0.0.0:5000(oruvicorn main:app --host 0.0.0.0 --port 5000). If it fails on your machine it will fail on the server.
Common failures and the fix
"No open ports detected" or a deploy that never goes live. The app is bound to 127.0.0.1, or to a hard-coded port instead of $PORT. Fix the start command and, if you use app.run anywhere, its host and port arguments.
ModuleNotFoundError: No module named 'flask'. The package is on your laptop but not in requirements.txt. Run pip freeze in your virtual environment and compare. The same error for gunicorn means you forgot to pin the server.
Failed to find attribute 'app' in 'app'. The module or variable name in app:app does not match the file. Check the filename and the name you assigned Flask(__name__) to.
First request times out or returns 502 after a quiet period. That is the free-tier cold start: Render stopped the instance after 15 idle minutes and needs about a minute to bring it back. Nothing is broken. Accept it for a portfolio project, move to a paid instance type, or use PythonAnywhere, whose free app does not sleep. Pinging your own service to keep it awake works but burns the 750-hour allowance faster if you run more than one service.
Where to go next
Build the app first, deploy second. The deploying Python web apps lesson puts the same environment-variable, port and start-command discipline into graded exercises. The Python in the lessons runs in your browser with no install; the deploy is the one step that needs a real host, and now you know which one to pick.