fastapi icon indicating copy to clipboard operation
fastapi copied to clipboard

Best practice to run FastAPI on Cloud Run with server port as $PORT

Open alexsantos opened this issue 2 years ago • 13 comments

First Check

  • [X] I added a very descriptive title to this issue.
  • [X] I used the GitHub search to find a similar issue and didn't find it.
  • [X] I searched the FastAPI documentation, with the integrated search.
  • [X] I already searched in Google "How to X in FastAPI" and didn't find any information.
  • [X] I already read and followed all the tutorial in the docs and didn't find an answer.
  • [X] I already checked if it is not related to FastAPI but to Pydantic.
  • [X] I already checked if it is not related to FastAPI but to Swagger UI.
  • [X] I already checked if it is not related to FastAPI but to ReDoc.

Commit to Help

  • [X] I commit to help with one of those options 👆

Example Code

"""
A sample Hello World server.
"""
import uvicorn

import os

from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")


@app.route('/')
def hello(request: Request):
    """Return a friendly HTTP greeting."""
    message = "It's running!"

    """Get Cloud Run environment variables."""
    service = os.environ.get('K_SERVICE', 'Unknown service')
    revision = os.environ.get('K_REVISION', 'Unknown revision')

    return templates.TemplateResponse('index.html', context={
        "request": request,
        "message": message,
        "Service": service,
        "Revision": revision})


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=int(os.environ.get('PORT', 8000)), log_level="info")

Description

I've been using Google Cloud Code in VSCode and PyCharm and the example they have for Python is a Flask app. I've been trying to rewrite the example app but with FastAPI. To have it running on Cloud Run you have to start the server with the port binded to an OS env variable called PORT. My question is:

  1. Should I start uvicorn programmaticaly like in the example or,
  2. Should I have a "normal" FastAPI app main.py file and use the CMD or ENTRYPOINT to bind the $PORT variable?

Note: ENTRYPOINT ["uvicorn", "main:app", "-port", "$PORT"] won't work because the CMD in exec form won't substitute the variable to its value, as documented here. It must be in shell form ENTRYPOINT uvicorn main:app --port $PORT

Operating System

Linux

Operating System Details

No response

FastAPI Version

0.78.0

Python Version

Python 3.10.4

Additional Context

The Cloud Code Flask sample code: https://github.com/GoogleCloudPlatform/cloud-code-samples/tree/v1/python/cloud-run-python-hello-world

alexsantos avatar Jun 17 '22 15:06 alexsantos

Do not run uvicorn alone. Read this docs from their page. https://www.uvicorn.org/deployment/

They recommend to run it with gunicorn and to use UvicornWorker: "Run gunicorn -k uvicorn.workers.UvicornWorker for production"

zoliknemet avatar Jun 17 '22 15:06 zoliknemet

Do not run uvicorn alone. Read this docs from their page. https://www.uvicorn.org/deployment/

They recommend to run it with gunicorn and to use UvicornWorker: "Run gunicorn -k uvicorn.workers.UvicornWorker for production"

From the FastAPI documentation: image

My question is about how should I pass the $PORT variable to the server (uvicorn/gunicorn) not about the server itself to use.

alexsantos avatar Jun 17 '22 16:06 alexsantos

AFAIK you are not able to reference host environment variables in your docker file, so the best way to achieve this is the example you gave. In reality, there isn't really a difference between using the a CLI command vs starting Uvicorn from within a Python file. Using a CLI would also start a Python process, which then would call the exact code as you have in your example (e.g. uvicorn.run()), they are interchangeable.

JarroVGIT avatar Jun 18 '22 00:06 JarroVGIT

Thanks for your feedback @JarroVGIT. What made me open this question was the fact that the entire documentation of FastAPI doesn't mention the option to start uvicorn from the python file. What about a new entry on the docs about this topic?

alexsantos avatar Jun 18 '22 10:06 alexsantos

I think (but am not sure!) that is because in the CLI you can have the -reload option that is lacking in the direct way, but for production usecases this wouldn’t make sense anyway :)

JarroVGIT avatar Jun 18 '22 11:06 JarroVGIT

You can use reload on uvicorn.run(). The thing is that the application path needs to be a string, and not the application object.

if __name__ == "__main__":
    uvicorn.run("main:app", reload=True)

Ref.: https://www.uvicorn.org/deployment/#running-programmatically

Kludex avatar Jun 19 '22 07:06 Kludex

Interesting, didn’t know you could pass a reload parameter, that will come in handy! Although my main pattern is to pass the application object itself, I might need to tweak my workflow a bit :)

JarroVGIT avatar Jun 19 '22 07:06 JarroVGIT

another derivative you can do is.

# runserver.py

from uvicorn import Config, Server
from fastapi import FastAPI

app = FastAPI()

if __name__ == "__main__":  # pragma: no cover
    server = Server(
        Config(
            "runserver:app",
            host="0.0.0.0",
            port=9002,
            reload=True,
        ),
    )
    
    # do something you want before running the server 
    # eg. setting up custom loggers

    server.run()

you may also want to consider using pydantic[dotenv] to fetch the env variables.

mpdevilleres avatar Jun 19 '22 07:06 mpdevilleres

Having multiple ways to achieve the same goal can be more confusing than helpful. 😅

Kludex avatar Jun 19 '22 07:06 Kludex

@Kludex

You are right,

Though I think this variant is worth mentioning. As he might be looking for a configuration step before running the server.

such as this scenario. https://github.com/tiangolo/fastapi/issues/1276#issuecomment-615877177

where loguru fails to intercept the first 3 entries in the log.

but yeah this might be too much answer for what has been asked.

mpdevilleres avatar Jun 19 '22 08:06 mpdevilleres

I have cloned the Google's example repository for Cloud Code but with FastAPI here: https://github.com/alexsantos/cloud-run-fastapi It is working both with the Dockerfile and with Cloud Build for Python.

alexsantos avatar Jun 20 '22 11:06 alexsantos

I have cloned the Google's example repository for Cloud Code but with FastAPI here: https://github.com/alexsantos/cloud-run-fastapi It is working both with the Dockerfile and with Cloud Build for Python.

@alexsantos thank you for sharing this! I'm curious how the Dockerfile + docker-compose.yml would look like if 1 had to link a cloud db? (at the moment, I'm trying to connect w/ AlloyDB, but when I am deploying it to CloudRun I receive a Error: Invalid value for '--port': '$PORT' is not a valid integer error on the cloud run logs) Any advice would be appreciated!

wjlee2020 avatar Dec 21 '22 01:12 wjlee2020

I have cloned the Google's example repository for Cloud Code but with FastAPI here: https://github.com/alexsantos/cloud-run-fastapi It is working both with the Dockerfile and with Cloud Build for Python.

@alexsantos thank you for sharing this! I'm curious how the Dockerfile + docker-compose.yml would look like if 1 had to link a cloud db? (at the moment, I'm trying to connect w/ AlloyDB, but when I am deploying it to CloudRun I receive a Error: Invalid value for '--port': '$PORT' is not a valid integer error on the cloud run logs) Any advice would be appreciated!

Hello and sorry for the delay: why are you using a docker-compose with a cloud DB?

alexsantos avatar Jan 15 '23 19:01 alexsantos