Skip to content

Search docs

Search the Skiffly documentation

Flask & 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:

DetectedStart command Railpack uses
fastapi + uvicorn in the dependencies, main.pyuvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}
flask + gunicorn in the dependencies, main.pygunicorn --bind 0.0.0.0:${PORT:-8000} main:app
otherwisepython 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 2

If 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 BackgroundTasks runs 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, sharing REDIS_URL and DATABASE_URL via 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#

SymptomFix
[railpack] start command: python main.py and the app exits immediatelythe 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 startupthe database service is still starting: retry in code or use pool_pre_ping=True; check skiffly status
Uploads vanish after a deployephemeral disk: mount a volume or use object storage

Example#

.skiffly/skiffly.ts
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