Flask e FastAPI
Deploy Flask, FastAPI, Starlette or any WSGI/ASGI app on Skiffly — what Railpack picks as the start command, gunicorn and uvicorn on `PORT`, workers, databases, background tasks and health checks.
Both frameworks are ordinary Python projects to Railpack; the Python & Django page explains dependency managers, the Python version and apt packages. What differs is the start command.
Start command#
Railpack derives one from the dependencies and the entry file:
| Detected | Start command Railpack uses |
|---|---|
fastapi + uvicorn in the dependencies, main.py | uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000} |
flask + gunicorn in the dependencies, main.py | gunicorn --bind 0.0.0.0:${PORT:-8000} main:app |
| otherwise | python main.py (then app.py, start.py, bot.py, hello.py, server.py, the first that exists) |
Two consequences: add the server to your dependencies (uvicorn[standard] or gunicorn), and name the module and app object as Railpack expects, or set the start command yourself in Settings → Deploy → Start command (startCommand, start: in config as code):
# FastAPI, module app/main.py, several workers
uvicorn app.main:app --host 0.0.0.0 --port $PORT --workers 2 --proxy-headers --forwarded-allow-ips='*'
# Flask with gunicorn, factory function
gunicorn 'myapp:create_app()' --bind 0.0.0.0:$PORT --workers 2 --threads 4
# FastAPI under gunicorn
gunicorn app.main:app -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:$PORT --workers 2If the start command is python main.py, read the port in code: uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 8000))) / app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8000))). Flask's development server is fine for a demo but gunicorn is the production choice.
Port#
PORT is the service port (8080 when the service does not set one). Bind to 0.0.0.0. --proxy-headers / ProxyFix make request.url use https and the public host: Skiffly's edge sets X-Forwarded-Proto and X-Forwarded-For.
# Flask
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)Databases#
skiffly deploy --template postgres
skiffly variables set 'DATABASE_URL=${{Postgres.DATABASE_URL}}'SQLAlchemy and SQLModel take DATABASE_URL directly (postgresql+psycopg://… if you want the psycopg 3 driver: replace the scheme in code, DATABASE_URL.replace("postgresql://", "postgresql+psycopg://", 1)). For async, asyncpg with postgresql+asyncpg://. Alembic migrations: sh -c "alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port $PORT" as the start command, or skiffly ssh -- alembic upgrade head as a one-off — there is no release phase. Flask-Migrate: flask db upgrade in the same place.
Redis for caching, rate limits or queues: skiffly deploy --template redis, then REDIS_URL=${{Redis.REDIS_URL}}.
Background work#
- Short tasks: FastAPI
BackgroundTasksruns inside the web process — fine for emails, not for minutes-long jobs. - Workers (Celery, RQ, arq, Dramatiq): a second service from the same repository with the start command
celery -A tasks worker, no domain, sharingREDIS_URLandDATABASE_URLvia references. - Scheduled scripts: a service with a cron schedule and the script as the start command (
python jobs/nightly.py).
Health check#
@app.get("/healthz")
def healthz():
return {"ok": True}Set the healthcheck path to /healthz. Keep it free of database calls; the node probes it every few seconds and restarts the container after three consecutive failures.
Workers and memory#
uvicorn --workers N or gunicorn --workers N forks N processes: with the default 2 vCPU / 2 GiB limit, 2 workers is a sensible start for FastAPI (async handles concurrency inside a worker); more for a blocking Flask app. Use replicas (Settings → Deploy → Replicas) rather than dozens of workers in one container.
Common problems#
| Symptom | Fix |
|---|---|
[railpack] start command: python main.py and the app exits immediately | the file only defines app: add uvicorn to the dependencies or set a start command |
FAILED after 5 minutes, uvicorn logs Uvicorn running on http://127.0.0.1:8000 | --host 0.0.0.0 --port $PORT |
ModuleNotFoundError: No module named 'app' | the module path in the start command does not match the repository layout (root directory setting, missing __init__.py) |
redirect_uri or generated links use http:// | enable proxy headers (--proxy-headers, ProxyFix) |
sqlalchemy.exc.OperationalError: could not connect at startup | the database service is still starting: retry in code or use pool_pre_ping=True; check skiffly status |
| Uploads vanish after a deploy | ephemeral disk: mount a volume or use object storage |
Example#
import { defineSkiffly, github, postgres, project, redis, service } from "@skiffly/config";
export default defineSkiffly(() => {
const db = postgres("db");
const cache = redis("cache");
const api = service("api", {
source: github("acme/api"),
start: "sh -c 'alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port $PORT --workers 2 --proxy-headers'",
healthcheck: "/healthz",
port: 8000,
env: { DATABASE_URL: db.env.DATABASE_URL, REDIS_URL: cache.env.REDIS_URL },
domain: true,
});
return project("acme-api", { resources: [db, cache, api] });
});Landings: /deploy/fastapi · /deploy/flask