|
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 | +#### Getting Started |
| 17 | + |
| 18 | +- Create a [DigitalOcean](mdc:https:/www.digitalocean.com) account |
| 19 | +- Install the [`doctl` CLI tool](mdc:https:/docs.digitalocean.com/reference/doctl) and authenticate with `doctl auth init` |
| 20 | +- Install the [`psql` client](mdc:https:/www.postgresql.org/download) |
| 21 | + |
| 22 | +#### Create a Project |
| 23 | + |
| 24 | +Create a new project to organize your resources: |
| 25 | + |
| 26 | +```bash |
| 27 | +# List existing projects |
| 28 | +doctl projects list |
| 29 | + |
| 30 | +# Create a new project |
| 31 | +doctl projects create --name "YOUR-PROJECT-NAME" --purpose "YOUR-PROJECT-PURPOSE" --environment "Production" |
| 32 | +``` |
| 33 | + |
| 34 | +#### Set Up a Managed PostgreSQL Database |
| 35 | + |
| 36 | +Create a managed, serverless PostgreSQL database instance: |
| 37 | + |
| 38 | +```bash |
| 39 | +doctl databases create your-db-name --engine pg --version 17 --size db-s-1vcpu-1gb --num-nodes 1 --wait |
| 40 | +``` |
| 41 | + |
| 42 | +Get the database ID from the output of the create command and use it to retrieve the database connection details: |
| 43 | + |
| 44 | +```bash |
| 45 | +# Get the database connection details |
| 46 | +doctl databases connection "your-database-id" --format Host,Port,User,Password,Database |
| 47 | +``` |
| 48 | + |
| 49 | +Store these details securely in a `.env.production` file (you will need to set them later in application deployment as production secrets): |
| 50 | + |
| 51 | +```bash |
| 52 | +# Database connection parameters |
| 53 | +DB_HOST=your-host |
| 54 | +DB_PORT=your-port |
| 55 | +DB_USER=your-user |
| 56 | +DB_PASS=your-password |
| 57 | +DB_NAME=your-database |
| 58 | +``` |
| 59 | + |
| 60 | +You may also want to save your database id, although you can always find it again later by listing your databases with `doctl databases list`. |
| 61 | + |
| 62 | +#### Setting Up a Firewall Rule (after Deploying Your Application Layer) |
| 63 | + |
| 64 | +Note that by default your database is publicly accessible from the Internet, so you should create a firewall rule to restrict access to only your application's IP address once you have deployed the application. The command to do this is: |
| 65 | + |
| 66 | +```bash |
| 67 | +doctl databases firewalls append <database-cluster-id> --rule <type>:<value> |
| 68 | +``` |
| 69 | + |
| 70 | +where `<type>` is `ip_addr` and `<value>` is the IP address of the application server. See the [DigitalOcean documentation](https://docs.digitalocean.com/reference/doctl/reference/databases/firewalls/append/) for more details. |
| 71 | + |
| 72 | +**Note:** You can only complete this step after you have deployed your application layer and obtained a static IP address for the application server. |
| 73 | + |
| 74 | +## Deploying and Configuring the FastAPI App |
| 75 | + |
| 76 | +### On Modal.com |
| 77 | + |
| 78 | +The big advantages of deploying on Modal.com are: |
| 79 | +1. that they offer $30/month of free credits for each user, plus generous additional free credit allotments for startups and researchers, and |
| 80 | +2. that it's a very user-friendly platform. |
| 81 | + |
| 82 | +The disadvantages are: |
| 83 | +1. that Modal is a Python-only platform and cannot run the database layer, so you'll have to deploy that somewhere else, |
| 84 | +2. that you'll need to make some modest changes to the codebase to get it to work on Modal, and |
| 85 | +3. that Modal offers a [static IP address for the application server](https://modal.com/docs/guide/proxy-ips) only if you pay for a higher-tier plan starting at $250/year, which makes securing the database layer with a firewall rule cost prohibitive. |
| 86 | + |
| 87 | +#### Getting Started |
| 88 | + |
| 89 | +- [Sign up for a Modal.com account](https://modal.com/signup) |
| 90 | +- Install modal in the project directory with `uv add modal` |
| 91 | +- Run `uv run modal setup` to authenticate with Modal |
| 92 | + |
| 93 | +#### Defining the Modal Image and App |
| 94 | + |
| 95 | +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. |
| 96 | + |
| 97 | +1. **Define the Modal Image in `deploy.py`:** |
| 98 | + - Use `modal.Image` to define the container environment. Chain methods to install dependencies and add code/files. |
| 99 | + - Start with a Debian base image matching your Python version (e.g., 3.13). |
| 100 | + - Install necessary system packages (`libpq-dev` for `psycopg2`, `libwebp-dev` for Pillow WebP support). |
| 101 | + - Install Python dependencies using `run_commands` with `uv`. |
| 102 | + - Add your local Python modules (`routers`, `utils`, `exceptions`) using `add_local_python_source`. |
| 103 | + - 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. |
| 104 | + |
| 105 | + ```python |
| 106 | + # deploy.py |
| 107 | + import modal |
| 108 | + import os |
| 109 | + |
| 110 | + # Define the base image |
| 111 | + image = ( |
| 112 | + modal.Image.debian_slim(python_version="3.13") |
| 113 | + .apt_install("libpq-dev", "libwebp-dev") |
| 114 | + .pip_install_from_pyproject("pyproject.toml") |
| 115 | + .add_local_python_source("main") |
| 116 | + .add_local_python_source("routers") |
| 117 | + .add_local_python_source("utils") |
| 118 | + .add_local_python_source("exceptions") |
| 119 | + .add_local_dir("static", remote_path="/root/static") |
| 120 | + .add_local_dir("templates", remote_path="/root/templates") |
| 121 | + ) |
| 122 | + |
| 123 | + # Define the Modal App |
| 124 | + app = modal.App( |
| 125 | + name="your-app-name", |
| 126 | + image=image, |
| 127 | + secrets=[modal.Secret.from_name("your-app-name-secret")] |
| 128 | + ) |
| 129 | + ``` |
| 130 | + |
| 131 | +2. **Define the ASGI App Function in `deploy.py`:** |
| 132 | + - Create a function decorated with `@app.function()` and `@modal.asgi_app()`. |
| 133 | + - Inside this function, import your FastAPI application instance from `main.py`. |
| 134 | + - Return the FastAPI app instance. |
| 135 | + - Use `@modal.concurrent()` to allow the container to handle multiple requests concurrently. |
| 136 | + |
| 137 | + ```python |
| 138 | + # deploy.py (continued) |
| 139 | + |
| 140 | + # Define the ASGI app function |
| 141 | + @app.function( |
| 142 | + allow_concurrent_inputs=100 # Adjust concurrency as needed |
| 143 | + ) |
| 144 | + @modal.asgi_app() |
| 145 | + def fastapi_app(): |
| 146 | + # Important: Import the app *inside* the function |
| 147 | + # This ensures it runs within the Modal container environment |
| 148 | + # and has access to the installed packages and secrets. |
| 149 | + # It also ensures the lifespan function (db setup) runs correctly |
| 150 | + # with the environment variables provided by the Modal Secret. |
| 151 | + from main import app as web_app |
| 152 | + |
| 153 | + return web_app |
| 154 | + ``` |
| 155 | + |
| 156 | +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). |
| 157 | + |
| 158 | +#### Deploying the App |
| 159 | + |
| 160 | +From your terminal, in the root directory of your project, run: |
| 161 | + |
| 162 | +```bash |
| 163 | +modal deploy deploy.py |
| 164 | +``` |
| 165 | + |
| 166 | +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`). |
| 167 | + |
| 168 | +#### Setting Up Modal Secrets |
| 169 | + |
| 170 | +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. |
| 171 | + |
| 172 | +Create a Modal Secret either through the Modal UI or CLI. Note that the name of the secret has to match the secret name you used in the `deploy.py` file, above (e.g., `your-app-name-secret`). |
| 173 | + |
| 174 | +```bash |
| 175 | +# Example using CLI |
| 176 | +modal secret create your-app-name-secret \ |
| 177 | + SECRET_KEY='your_actual_secret_key' \ |
| 178 | + DB_USER='your_db_user' \ |
| 179 | + DB_PASSWORD='your_db_password' \ |
| 180 | + DB_HOST='your_external_db_host' \ |
| 181 | + DB_PORT='your_db_port' \ |
| 182 | + DB_NAME='your_db_name' \ |
| 183 | + RESEND_API_KEY='your_resend_api_key' \ |
| 184 | + BASE_URL='https://your-username--your-app-name-serve.modal.run' |
| 185 | +``` |
| 186 | + |
| 187 | +**Important:** Ensure `DB_HOST` points to your *cloud* database host address, not `localhost` or `host.docker.internal`. |
| 188 | + |
| 189 | +#### Testing the Deployment |
| 190 | + |
| 191 | +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. |
0 commit comments