|
2 | 2 | title: "Deployment"
|
3 | 3 | ---
|
4 | 4 |
|
5 |
| -## Under construction |
| 5 | +This application requires two services to be deployed and connected to each other: |
| 6 | + |
| 7 | +1. A PostgreSQL database (the storage layer) |
| 8 | +2. A FastAPI app (the application layer) |
| 9 | + |
| 10 | +There are *many* hosting options available for each of these services; this guide will cover only a few of them. |
| 11 | + |
| 12 | +## Deploying and Configuring the PostgreSQL Database |
| 13 | + |
| 14 | +### On Digital Ocean |
| 15 | + |
| 16 | +## Deploying and Configuring the FastAPI App |
| 17 | + |
| 18 | +### On Modal.com |
| 19 | + |
| 20 | +The big advantages of deploying on Modal.com are: |
| 21 | +1. that they offer $30/month of free credits for each user, plus generous additional free credit allotments for startups and researchers, and |
| 22 | +2. that it's a very user-friendly platform. |
| 23 | + |
| 24 | +The disadvantages are: |
| 25 | +1. that Modal is a Python-only platform and cannot run the database layer, so you'll have to deploy that somewhere else, and |
| 26 | +2. that you'll need to make some modest changes to the codebase to get it to work on Modal. |
| 27 | + |
| 28 | +#### Getting Started |
| 29 | + |
| 30 | +- [Sign up for a Modal.com account](https://modal.com/signup) |
| 31 | +- Install modal in the project directory with `uv add modal` |
| 32 | +- Run `uv run modal setup` to authenticate with Modal |
| 33 | + |
| 34 | +#### Setting Up Modal Secrets |
| 35 | + |
| 36 | +The application relies on environment variables stored in `.env` (like `SECRET_KEY`, `DB_USER`, `DB_PASSWORD`, `DB_HOST`, `DB_PORT`, `DB_NAME`, `RESEND_API_KEY`, `BASE_URL`). These sensitive values should be stored securely using Modal Secrets. |
| 37 | + |
| 38 | +Create a Modal Secret (e.g., named `fastapi-webapp-secrets`) either through the Modal UI or CLI: |
| 39 | +```bash |
| 40 | +# Example using CLI |
| 41 | +modal secret create your-app-name-secret \ |
| 42 | + SECRET_KEY='your_actual_secret_key' \ |
| 43 | + DB_USER='your_db_user' \ |
| 44 | + DB_PASSWORD='your_db_password' \ |
| 45 | + DB_HOST='your_external_db_host' \ |
| 46 | + DB_PORT='your_db_port' \ |
| 47 | + DB_NAME='your_db_name' \ |
| 48 | + RESEND_API_KEY='your_resend_api_key' \ |
| 49 | + BASE_URL='https://your-modal-app-url' # Usually https://your-org-name--your-app-name-serve.modal.run |
| 50 | +``` |
| 51 | + |
| 52 | +**Important:** Ensure `DB_HOST` points to your *cloud* database host address, not `localhost` or `host.docker.internal`. |
| 53 | + |
| 54 | +#### Defining the Modal Image and App |
| 55 | + |
| 56 | +Create a new Python file in the root of your project, for example, `deploy.py`. This file will define the Modal Image and the ASGI app deployment. |
| 57 | + |
| 58 | +1. **Define the Modal Image in `deploy.py`:** |
| 59 | + - Use `modal.Image` to define the container environment. Chain methods to install dependencies and add code/files. |
| 60 | + - Start with a Debian base image matching your Python version (e.g., 3.13). |
| 61 | + - Install necessary system packages (`libpq-dev` for `psycopg2`, `libwebp-dev` for Pillow WebP support). |
| 62 | + - Install Python dependencies using `run_commands` with `uv`. |
| 63 | + - Add your local Python modules (`routers`, `utils`, `exceptions`) using `add_local_python_source`. |
| 64 | + - Add the `static` and `templates` directories using `add_local_dir`. The default behaviour (copying on container startup) is usually fine for development, but consider `copy=True` for production stability if these files are large or rarely change. |
| 65 | + |
| 66 | + ```python |
| 67 | + # deploy.py |
| 68 | + import modal |
| 69 | + import os |
| 70 | + |
| 71 | + # Define the base image |
| 72 | + image = ( |
| 73 | + modal.Image.debian_slim(python_version="3.13") |
| 74 | + .apt_install("libpq-dev", "libwebp-dev") |
| 75 | + .pip_install("uv") |
| 76 | + .run_commands( |
| 77 | + # Install dependencies from pyproject.toml using uv |
| 78 | + # Make sure uv sync handles psycopg2 correctly with libpq-dev present |
| 79 | + "uv sync --no-dev --system --compile-bytecode", |
| 80 | + ) |
| 81 | + .add_local_python_source("routers") |
| 82 | + .add_local_python_source("utils") |
| 83 | + .add_local_python_source("exceptions") |
| 84 | + .add_local_dir("static", remote_path="/static") |
| 85 | + .add_local_dir("templates", remote_path="/templates") |
| 86 | + ) |
| 87 | + |
| 88 | + # Define the Modal App |
| 89 | + app = modal.App( |
| 90 | + name="fastapi-webapp", # Optional: name for your Modal app |
| 91 | + image=image, |
| 92 | + secrets=[modal.Secret.from_name("fastapi-webapp-secrets")] |
| 93 | + ) |
| 94 | + ``` |
| 95 | + |
| 96 | +2. **Define the ASGI App Function in `deploy.py`:** |
| 97 | + - Create a function decorated with `@app.function()` and `@modal.asgi_app()`. |
| 98 | + - Inside this function, import your FastAPI application instance from `main.py`. |
| 99 | + - Return the FastAPI app instance. |
| 100 | + - Use `@modal.concurrent()` to allow the container to handle multiple requests concurrently. |
| 101 | + |
| 102 | + ```python |
| 103 | + # deploy.py (continued) |
| 104 | + |
| 105 | + # Define the ASGI app function |
| 106 | + @app.function( |
| 107 | + allow_concurrent_inputs=100 # Adjust concurrency as needed |
| 108 | + ) |
| 109 | + @modal.asgi_app() |
| 110 | + def fastapi_app(): |
| 111 | + # Important: Import the app *inside* the function |
| 112 | + # This ensures it runs within the Modal container environment |
| 113 | + # and has access to the installed packages and secrets. |
| 114 | + # It also ensures the lifespan function (db setup) runs correctly |
| 115 | + # with the environment variables provided by the Modal Secret. |
| 116 | + from main import app as web_app |
| 117 | + |
| 118 | + return web_app |
| 119 | + ``` |
| 120 | + |
| 121 | +For more information on Modal FastAPI images and applications, see [this guide](https://modal.com/docs/guide/webhooks#how-do-web-endpoints-run-in-the-cloud). |
| 122 | + |
| 123 | +#### Deploying the App |
| 124 | + |
| 125 | +1. From your terminal, in the root directory of your project, run: |
| 126 | + ```bash |
| 127 | + modal deploy deploy.py |
| 128 | + ``` |
| 129 | + Modal will build the image (if it hasn't been built before or if dependencies changed) and deploy the ASGI app. It will output a public URL (e.g., `https://your-username--your-app-name.modal.run`). |
| 130 | + |
| 131 | +2. Once you have the public URL from Modal, make sure the `BASE_URL` variable in your Modal Secret matches this public URL. This is crucial for generating correct absolute URLs (e.g., in password reset emails). If you need to update the secret, run: |
| 132 | + ```bash |
| 133 | + modal secret update your-app-name-secret BASE_URL='https://your-modal-app-url' |
| 134 | + ``` |
| 135 | + Redeploy if necessary (`modal deploy deploy.py`), although changes to secrets often take effect on subsequent container starts without a full redeploy. |
| 136 | + |
| 137 | +3. Access the provided Modal URL in your browser. Browse the site and test the registration and password reset features to ensure database and Resend connections work correctly. |
0 commit comments