|
1 | 1 | # FastAPI, Jinja2, PostgreSQL Webapp Template
|
2 | 2 |
|
3 |
| - |
| 3 | + |
4 | 4 |
|
5 | 5 | ## Quickstart
|
6 | 6 |
|
@@ -114,7 +114,7 @@ Generate a 256 bit secret key with `openssl rand -base64 32` and paste it into t
|
114 | 114 |
|
115 | 115 | Set your desired database name, username, and password in the .env file.
|
116 | 116 |
|
117 |
| -To use password recovery, register a [Resend](https://resend.com/) account, verify a domain, get an API key, and paste the API key into the .env file. |
| 117 | +To use password recovery and other email features, register a [Resend](https://resend.com/) account, verify a domain, get an API key, and paste the API key and the email address you want to send emails from into the .env file. Note that you will need to [verify a domain through the Resend dashboard](https://resend.com/docs/dashboard/domains/introduction) to send emails from that domain. |
118 | 118 |
|
119 | 119 | ### Start development database
|
120 | 120 |
|
@@ -542,7 +542,7 @@ Generate a 256 bit secret key with `openssl rand -base64 32` and paste it into t
|
542 | 542 |
|
543 | 543 | Set your desired database name, username, and password in the .env file.
|
544 | 544 |
|
545 |
| -To use password recovery, register a [Resend](https://resend.com/) account, verify a domain, get an API key, and paste the API key into the .env file. |
| 545 | +To use password recovery, register a [Resend](https://resend.com/) account, verify a domain, get an API key, and paste the API key and sender email address into the .env file. |
546 | 546 |
|
547 | 547 | If using the dev container configuration, you will need to set the `DB_HOST` environment variable to "host.docker.internal" in the .env file. Otherwise, set `DB_HOST` to "localhost" for local development. (In production, `DB_HOST` will be set to the hostname of the database server.)
|
548 | 548 |
|
@@ -988,7 +988,193 @@ Server-side validation remains essential as a security measure against malicious
|
988 | 988 |
|
989 | 989 | # Deployment
|
990 | 990 |
|
991 |
| -## Under construction |
| 991 | +This application requires two services to be deployed and connected to each other: |
| 992 | + |
| 993 | +1. A PostgreSQL database (the storage layer) |
| 994 | +2. A FastAPI app (the application layer) |
| 995 | + |
| 996 | +There are *many* hosting options available for each of these services; this guide will cover only a few of them. |
| 997 | + |
| 998 | +## Deploying and Configuring the PostgreSQL Database |
| 999 | + |
| 1000 | +### On Digital Ocean |
| 1001 | + |
| 1002 | +#### Getting Started |
| 1003 | + |
| 1004 | +- Create a [DigitalOcean](mdc:https:/www.digitalocean.com) account |
| 1005 | +- Install the [`doctl` CLI tool](mdc:https:/docs.digitalocean.com/reference/doctl) and authenticate with `doctl auth init` |
| 1006 | +- Install the [`psql` client](mdc:https:/www.postgresql.org/download) |
| 1007 | + |
| 1008 | +#### Create a Project |
| 1009 | + |
| 1010 | +Create a new project to organize your resources: |
| 1011 | + |
| 1012 | +```bash |
| 1013 | +# List existing projects |
| 1014 | +doctl projects list |
| 1015 | + |
| 1016 | +# Create a new project |
| 1017 | +doctl projects create --name "YOUR-PROJECT-NAME" --purpose "YOUR-PROJECT-PURPOSE" --environment "Production" |
| 1018 | +``` |
| 1019 | + |
| 1020 | +#### Set Up a Managed PostgreSQL Database |
| 1021 | + |
| 1022 | +Create a managed, serverless PostgreSQL database instance: |
| 1023 | + |
| 1024 | +```bash |
| 1025 | +doctl databases create your-db-name --engine pg --version 17 --size db-s-1vcpu-1gb --num-nodes 1 --wait |
| 1026 | +``` |
| 1027 | + |
| 1028 | +Get the database ID from the output of the create command and use it to retrieve the database connection details: |
| 1029 | + |
| 1030 | +```bash |
| 1031 | +# Get the database connection details |
| 1032 | +doctl databases connection "your-database-id" --format Host,Port,User,Password,Database |
| 1033 | +``` |
| 1034 | + |
| 1035 | +Store these details securely in a `.env.production` file (you will need to set them later in application deployment as production secrets): |
| 1036 | + |
| 1037 | +```bash |
| 1038 | +# Database connection parameters |
| 1039 | +DB_HOST=your-host |
| 1040 | +DB_PORT=your-port |
| 1041 | +DB_USER=your-user |
| 1042 | +DB_PASS=your-password |
| 1043 | +DB_NAME=your-database |
| 1044 | +``` |
| 1045 | + |
| 1046 | +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`. |
| 1047 | + |
| 1048 | +#### Setting Up a Firewall Rule (after Deploying Your Application Layer) |
| 1049 | + |
| 1050 | +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: |
| 1051 | + |
| 1052 | +```bash |
| 1053 | +doctl databases firewalls append <database-cluster-id> --rule <type>:<value> |
| 1054 | +``` |
| 1055 | + |
| 1056 | +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. |
| 1057 | + |
| 1058 | +**Note:** You can only complete this step after you have deployed your application layer and obtained a static IP address for the application server. |
| 1059 | + |
| 1060 | +## Deploying and Configuring the FastAPI App |
| 1061 | + |
| 1062 | +### On Modal.com |
| 1063 | + |
| 1064 | +The big advantages of deploying on Modal.com are: |
| 1065 | +1. that they offer $30/month of free credits for each user, plus generous additional free credit allotments for startups and researchers, and |
| 1066 | +2. that it's a very user-friendly platform. |
| 1067 | + |
| 1068 | +The disadvantages are: |
| 1069 | +1. that Modal is a Python-only platform and cannot run the database layer, so you'll have to deploy that somewhere else, |
| 1070 | +2. that you'll need to make some modest changes to the codebase to get it to work on Modal, and |
| 1071 | +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. |
| 1072 | + |
| 1073 | +#### Getting Started |
| 1074 | + |
| 1075 | +- [Sign up for a Modal.com account](https://modal.com/signup) |
| 1076 | +- Install modal in the project directory with `uv add modal` |
| 1077 | +- Run `uv run modal setup` to authenticate with Modal |
| 1078 | + |
| 1079 | +#### Defining the Modal Image and App |
| 1080 | + |
| 1081 | +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. |
| 1082 | + |
| 1083 | +1. **Define the Modal Image in `deploy.py`:** |
| 1084 | + - Use `modal.Image` to define the container environment. Chain methods to install dependencies and add code/files. |
| 1085 | + - Start with a Debian base image matching your Python version (e.g., 3.13). |
| 1086 | + - Install necessary system packages (`libpq-dev` for `psycopg2`, `libwebp-dev` for Pillow WebP support). |
| 1087 | + - Install Python dependencies using `run_commands` with `uv`. |
| 1088 | + - Add your local Python modules (`routers`, `utils`, `exceptions`) using `add_local_python_source`. |
| 1089 | + - 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. |
| 1090 | + |
| 1091 | + ```python |
| 1092 | + # deploy.py |
| 1093 | + import modal |
| 1094 | + import os |
| 1095 | + |
| 1096 | + # Define the base image |
| 1097 | + image = ( |
| 1098 | + modal.Image.debian_slim(python_version="3.13") |
| 1099 | + .apt_install("libpq-dev", "libwebp-dev") |
| 1100 | + .pip_install_from_pyproject("pyproject.toml") |
| 1101 | + .add_local_python_source("main") |
| 1102 | + .add_local_python_source("routers") |
| 1103 | + .add_local_python_source("utils") |
| 1104 | + .add_local_python_source("exceptions") |
| 1105 | + .add_local_dir("static", remote_path="/root/static") |
| 1106 | + .add_local_dir("templates", remote_path="/root/templates") |
| 1107 | + ) |
| 1108 | + |
| 1109 | + # Define the Modal App |
| 1110 | + app = modal.App( |
| 1111 | + name="your-app-name", |
| 1112 | + image=image, |
| 1113 | + secrets=[modal.Secret.from_name("your-app-name-secret")] |
| 1114 | + ) |
| 1115 | + ``` |
| 1116 | + |
| 1117 | +2. **Define the ASGI App Function in `deploy.py`:** |
| 1118 | + - Create a function decorated with `@app.function()` and `@modal.asgi_app()`. |
| 1119 | + - Inside this function, import your FastAPI application instance from `main.py`. |
| 1120 | + - Return the FastAPI app instance. |
| 1121 | + - Use `@modal.concurrent()` to allow the container to handle multiple requests concurrently. |
| 1122 | + |
| 1123 | + ```python |
| 1124 | + # deploy.py (continued) |
| 1125 | + |
| 1126 | + # Define the ASGI app function |
| 1127 | + @app.function( |
| 1128 | + allow_concurrent_inputs=100 # Adjust concurrency as needed |
| 1129 | + ) |
| 1130 | + @modal.asgi_app() |
| 1131 | + def fastapi_app(): |
| 1132 | + # Important: Import the app *inside* the function |
| 1133 | + # This ensures it runs within the Modal container environment |
| 1134 | + # and has access to the installed packages and secrets. |
| 1135 | + # It also ensures the lifespan function (db setup) runs correctly |
| 1136 | + # with the environment variables provided by the Modal Secret. |
| 1137 | + from main import app as web_app |
| 1138 | + |
| 1139 | + return web_app |
| 1140 | + ``` |
| 1141 | + |
| 1142 | +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). |
| 1143 | + |
| 1144 | +#### Deploying the App |
| 1145 | + |
| 1146 | +From your terminal, in the root directory of your project, run: |
| 1147 | + |
| 1148 | +```bash |
| 1149 | +modal deploy deploy.py |
| 1150 | +``` |
| 1151 | + |
| 1152 | +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`). |
| 1153 | + |
| 1154 | +#### Setting Up Modal Secrets |
| 1155 | + |
| 1156 | +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. |
| 1157 | + |
| 1158 | +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`). |
| 1159 | + |
| 1160 | +```bash |
| 1161 | +# Example using CLI |
| 1162 | +modal secret create your-app-name-secret \ |
| 1163 | + SECRET_KEY='your_actual_secret_key' \ |
| 1164 | + DB_USER='your_db_user' \ |
| 1165 | + DB_PASSWORD='your_db_password' \ |
| 1166 | + DB_HOST='your_external_db_host' \ |
| 1167 | + DB_PORT='your_db_port' \ |
| 1168 | + DB_NAME='your_db_name' \ |
| 1169 | + RESEND_API_KEY='your_resend_api_key' \ |
| 1170 | + BASE_URL='https://your-username--your-app-name-serve.modal.run' |
| 1171 | +``` |
| 1172 | + |
| 1173 | +**Important:** Ensure `DB_HOST` points to your *cloud* database host address, not `localhost` or `host.docker.internal`. |
| 1174 | + |
| 1175 | +#### Testing the Deployment |
| 1176 | + |
| 1177 | +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. |
992 | 1178 |
|
993 | 1179 | # Contributing
|
994 | 1180 |
|
|
0 commit comments