Search code examples
dockerherokufastapiuvicorn

Host an API on Heroku using a manual docker build


I want to host a small web app on the Heroku free tier. I'm using docker compose to run the frontend, backend api, and a postgres DB locally. I am following the Heroku docs on deploying an existing docker image. However, when I try access the API docs, or try curl the API (both of which work locally), I get the error

2021-01-31T15:19:23.679917+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/docs" host=apitest48398.herokuapp.com request_id=62ffd495-5977-4132-9ec7-d89abf5b1d8f fwd="77.100.21.140" dyno= connect= service= status=503 bytes= protocol=https
2021-01-31T15:19:24.465326+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/favicon.ico" host=apitest48398.herokuapp.com request_id=016de415-dc8e-4b0a-894e-8fad37ff7fee fwd="77.100.21.140" dyno= connect= service= status=503 bytes= protocol=https

Note I am deploying via the container registry, not by connecting my git repo to Heroku, hence neither a Procfile, nor a heroku.yml file.

Here are the steps to reproduce this behaviour in a minimal example. Note that in the real example, I have multiple docker files, so even though this simple example could have been deployed using heroku container:push, I prefer to build the images myself so the simple example is more representative of the real use case:

  1. Create the app using fastAPI in a file called main.py:
    from fastapi import FastAPI
    
    app = FastAPI()
    
    
    @app.get("/")
    async def root():
        return {"message": "Hello World"}
    
  2. Create a requirements.txt file:
    fastapi
    uvicorn
    
  3. Create the Dockerfile:
    FROM python:3.7
    
    ENV PORT=$PORT
    
    COPY ./requirements.txt .
    RUN pip install -r requirements.txt
    
    COPY ./main.py .
    
    EXPOSE $PORT
    
    CMD uvicorn main:app --host 0.0.0.0 --port $PORT
    
    
  4. build the image
    docker build . -t herokubackendtest
    
  5. log in to heroku
    heroku container:login
    
  6. Create the heroku app and push the image to heroku (you'd need to use a different, unique APP_NAME)
    export APP_NAME=apitest48398
    heroku create $APP_NAME
    docker tag herokubackendtest registry.heroku.com/$APP_NAME/api
    docker push registry.heroku.com/$APP_NAME/api
    
  7. Release the app, scale up a dyno, check that the process is running, and check the logs to verify the uvicorn is running:
    heroku container:release --app $APP_NAME api
    heroku ps:scale --app $APP_NAME api=1
    heroku ps --app $APP_NAME  # should show === api (Free): /bin/sh -c uvicorn\ main:app\ --host\ 0.0.0.0\ --port\ \$PORT (1)
    heroku logs --app $APP_NAME --tail
    

You should see the following in the logs (the port number will vary)

2021-01-31T15:17:39.806481+00:00 heroku[api.1]: Starting process with command `/bin/sh -c uvicorn\ main:app\ --host\ 0.0.0.0\ --port\ \16384`
2021-01-31T15:17:40.465132+00:00 heroku[api.1]: State changed from starting to up
2021-01-31T15:17:41.982857+00:00 app[api.1]: INFO:     Started server process [5]
2021-01-31T15:17:41.982918+00:00 app[api.1]: INFO:     Waiting for application startup.
2021-01-31T15:17:41.983162+00:00 app[api.1]: INFO:     Application startup complete.
2021-01-31T15:17:41.983529+00:00 app[api.1]: INFO:     Uvicorn running on http://0.0.0.0:16384 (Press CTRL+C to quit)

Now to test it, you should be able to see the openAPI (swagger) docs at https://apitest48398.herokuapp.com/docs where apitest48398 is your $APP_NAME. But instead I get the error in the logs I posted at the start of this question, and I see a page that says:

Application error
An error occurred in the application and your page could not be served. If you are the application owner, check your logs for details. You can do this from the Heroku CLI with the command
heroku logs --tail

in my browser.

I also would expect curl https://apitest48398.herokuapp.com/ to return my hello world json, but it instead returns some HTML with an error. I've also tried with ports 443, 80, and the port uvicorn is running on inside the container (this one just hangs until timeout).

I assume I have missed some step, maybe with SSL? Although I understood that this was automatically enabled for any herokuapp.com domains on the free tier, and I can't see any way to pass the cert and pem files through to uvicorn.

Can anyone point me in the right direction as to how to resolve this?


Solution

  • Tag the image using the web process type, as indicated by Heroku Docker Registry. In your case

    docker tag herokubackendtest registry.heroku.com/$APP_NAME/web
    docker push registry.heroku.com/$APP_NAME/web
    heroku container:release web --app $APP_NAME