👷 Add continuous deployment and refactors needed for it (#667)
This commit is contained in:

committed by
GitHub

parent
bb7da40c87
commit
b9cbb4f8f4
17
.env
17
.env
@@ -1,11 +1,11 @@
|
||||
# Update this with your app domain
|
||||
# Domain
|
||||
# This would be set to the production domain with an env var on deployment
|
||||
DOMAIN=localhost
|
||||
# DOMAIN=localhost.tiangolo.com
|
||||
|
||||
SERVER_HOST=http://localhost
|
||||
# Environment: local, staging, production
|
||||
ENVIRONMENT=local
|
||||
|
||||
PROJECT_NAME="FastAPI Project"
|
||||
|
||||
STACK_NAME=fastapi-project
|
||||
|
||||
# Backend
|
||||
@@ -13,6 +13,9 @@ BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,h
|
||||
SECRET_KEY=changethis
|
||||
FIRST_SUPERUSER=admin@example.com
|
||||
FIRST_SUPERUSER_PASSWORD=changethis
|
||||
USERS_OPEN_REGISTRATION=False
|
||||
|
||||
# Emails
|
||||
SMTP_HOST=
|
||||
SMTP_USER=
|
||||
SMTP_PASSWORD=
|
||||
@@ -20,12 +23,10 @@ EMAILS_FROM_EMAIL=info@example.com
|
||||
SMTP_TLS=True
|
||||
SMTP_PORT=587
|
||||
|
||||
USERS_OPEN_REGISTRATION=False
|
||||
|
||||
# Postgres
|
||||
POSTGRES_SERVER=db
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_SERVER=localhost
|
||||
POSTGRES_DB=app
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=changethis
|
||||
|
||||
# PgAdmin
|
||||
|
32
.github/workflows/deploy-production.yml
vendored
Normal file
32
.github/workflows/deploy-production.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Deploy to Production
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- production
|
||||
env:
|
||||
ENVIRONMENT: production
|
||||
DOMAIN: ${{ secrets.DOMAIN_PRODUCTION }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }}
|
||||
FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }}
|
||||
SMTP_HOST: ${{ secrets.SMTP_HOST }}
|
||||
SMTP_USER: ${{ secrets.SMTP_USER }}
|
||||
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
|
||||
EMAILS_FROM_EMAIL: ${{ secrets.EMAILS_FROM_EMAIL }}
|
||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||
PGADMIN_DEFAULT_EMAIL: ${{ secrets.PGADMIN_DEFAULT_EMAIL }}
|
||||
PGADMIN_DEFAULT_PASSWORD: ${{ secrets.PGADMIN_DEFAULT_PASSWORD }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
FLOWER_BASIC_AUTH: ${{ secrets.FLOWER_BASIC_AUTH }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- run: docker compose -f docker-compose.yml build
|
||||
- run: docker compose -f docker-compose.yml up -d
|
32
.github/workflows/deploy-staging.yml
vendored
Normal file
32
.github/workflows/deploy-staging.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Deploy to Staging
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- staging
|
||||
env:
|
||||
ENVIRONMENT: staging
|
||||
DOMAIN: ${{ secrets.DOMAIN_STAGING }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }}
|
||||
FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }}
|
||||
SMTP_HOST: ${{ secrets.SMTP_HOST }}
|
||||
SMTP_USER: ${{ secrets.SMTP_USER }}
|
||||
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
|
||||
EMAILS_FROM_EMAIL: ${{ secrets.EMAILS_FROM_EMAIL }}
|
||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||
PGADMIN_DEFAULT_EMAIL: ${{ secrets.PGADMIN_DEFAULT_EMAIL }}
|
||||
PGADMIN_DEFAULT_PASSWORD: ${{ secrets.PGADMIN_DEFAULT_PASSWORD }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
FLOWER_BASIC_AUTH: ${{ secrets.FLOWER_BASIC_AUTH }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- run: docker compose -f docker-compose.yml build
|
||||
- run: docker compose -f docker-compose.yml up -d
|
@@ -146,7 +146,6 @@ But don't worry, you can just update any of that in the `.env` files afterwards.
|
||||
|
||||
The input variables, with their default values (some auto generated) are:
|
||||
|
||||
- `domain`: (default: `"localhost"`) Which domain name to use for the project, by default, localhost, but you should change it later (in .env).
|
||||
- `project_name`: (default: `"FastAPI Project"`) The name of the project, shown to API users (in .env).
|
||||
- `stack_name`: (default: `"fastapi-project"`) The name of the stack used for Docker Compose labels (no spaces) (in .env).
|
||||
- `secret_key`: (default: `"changethis"`) The secret key for the project, used for security, stored in .env, you can generate one with the method above.
|
||||
|
@@ -60,7 +60,7 @@ def create_user(*, session: SessionDep, user_in: UserCreate) -> Any:
|
||||
)
|
||||
|
||||
user = crud.create_user(session=session, user_create=user_in)
|
||||
if settings.EMAILS_ENABLED and user_in.email:
|
||||
if settings.emails_enabled and user_in.email:
|
||||
email_data = generate_new_account_email(
|
||||
email_to=user_in.email, username=user_in.email, password=user_in.password
|
||||
)
|
||||
|
@@ -1,63 +1,68 @@
|
||||
import secrets
|
||||
from typing import Any
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from pydantic import (
|
||||
AnyHttpUrl,
|
||||
AnyUrl,
|
||||
BeforeValidator,
|
||||
HttpUrl,
|
||||
PostgresDsn,
|
||||
ValidationInfo,
|
||||
field_validator,
|
||||
computed_field,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic_core import MultiHostUrl
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
def parse_cors(v: Any) -> list[str] | str:
|
||||
if isinstance(v, str) and not v.startswith("["):
|
||||
return [i.strip() for i in v.split(",")]
|
||||
elif isinstance(v, list | str):
|
||||
return v
|
||||
raise ValueError(v)
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file=".env", env_ignore_empty=True)
|
||||
API_V1_STR: str = "/api/v1"
|
||||
SECRET_KEY: str = secrets.token_urlsafe(32)
|
||||
# 60 minutes * 24 hours * 8 days = 8 days
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
|
||||
SERVER_HOST: AnyHttpUrl
|
||||
BACKEND_CORS_ORIGINS: list[AnyHttpUrl] | str = []
|
||||
DOMAIN: str = "localhost"
|
||||
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
|
||||
|
||||
@field_validator("BACKEND_CORS_ORIGINS", mode="before")
|
||||
@classmethod
|
||||
def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str:
|
||||
if isinstance(v, str) and not v.startswith("["):
|
||||
return [i.strip() for i in v.split(",")]
|
||||
elif isinstance(v, list | str):
|
||||
return v
|
||||
raise ValueError(v)
|
||||
@computed_field # type: ignore[misc]
|
||||
@property
|
||||
def server_host(self) -> str:
|
||||
# Use HTTPS for anything other than local development
|
||||
if self.ENVIRONMENT == "local":
|
||||
return f"http://{self.DOMAIN}"
|
||||
return f"https://{self.DOMAIN}"
|
||||
|
||||
BACKEND_CORS_ORIGINS: Annotated[
|
||||
list[AnyUrl] | str, BeforeValidator(parse_cors)
|
||||
] = []
|
||||
|
||||
PROJECT_NAME: str
|
||||
SENTRY_DSN: HttpUrl | None = None
|
||||
|
||||
@field_validator("SENTRY_DSN", mode="before")
|
||||
@classmethod
|
||||
def sentry_dsn_can_be_blank(cls, v: str) -> str | None:
|
||||
if not v:
|
||||
return None
|
||||
return v
|
||||
|
||||
POSTGRES_SERVER: str
|
||||
POSTGRES_USER: str
|
||||
POSTGRES_PASSWORD: str
|
||||
POSTGRES_DB: str
|
||||
SQLALCHEMY_DATABASE_URI: PostgresDsn | None = None
|
||||
POSTGRES_DB: str = ""
|
||||
|
||||
@field_validator("SQLALCHEMY_DATABASE_URI", mode="before")
|
||||
def assemble_db_connection(cls, v: str | None, info: ValidationInfo) -> Any:
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
return PostgresDsn.build(
|
||||
@computed_field # type: ignore[misc]
|
||||
@property
|
||||
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
|
||||
return MultiHostUrl.build(
|
||||
scheme="postgresql+psycopg",
|
||||
username=info.data.get("POSTGRES_USER"),
|
||||
password=info.data.get("POSTGRES_PASSWORD"),
|
||||
host=info.data.get("POSTGRES_SERVER"),
|
||||
path=f"{info.data.get('POSTGRES_DB') or ''}",
|
||||
username=self.POSTGRES_USER,
|
||||
password=self.POSTGRES_PASSWORD,
|
||||
host=self.POSTGRES_SERVER,
|
||||
path=self.POSTGRES_DB,
|
||||
)
|
||||
|
||||
SMTP_TLS: bool = True
|
||||
SMTP_PORT: int | None = None
|
||||
SMTP_PORT: int = 587
|
||||
SMTP_HOST: str | None = None
|
||||
SMTP_USER: str | None = None
|
||||
SMTP_PASSWORD: str | None = None
|
||||
@@ -65,23 +70,19 @@ class Settings(BaseSettings):
|
||||
EMAILS_FROM_EMAIL: str | None = None
|
||||
EMAILS_FROM_NAME: str | None = None
|
||||
|
||||
@field_validator("EMAILS_FROM_NAME")
|
||||
def get_project_name(cls, v: str | None, info: ValidationInfo) -> str:
|
||||
if not v:
|
||||
return str(info.data["PROJECT_NAME"])
|
||||
return v
|
||||
@model_validator(mode="after")
|
||||
def set_default_emails_from(self) -> Self:
|
||||
if not self.EMAILS_FROM_NAME:
|
||||
self.EMAILS_FROM_NAME = self.PROJECT_NAME
|
||||
return self
|
||||
|
||||
EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
|
||||
EMAIL_TEMPLATES_DIR: str = "/app/app/email-templates/build"
|
||||
EMAILS_ENABLED: bool = False
|
||||
|
||||
@field_validator("EMAILS_ENABLED", mode="before")
|
||||
def get_emails_enabled(cls, v: bool, info: ValidationInfo) -> bool:
|
||||
return bool(
|
||||
info.data.get("SMTP_HOST")
|
||||
and info.data.get("SMTP_PORT")
|
||||
and info.data.get("EMAILS_FROM_EMAIL")
|
||||
)
|
||||
@computed_field # type: ignore[misc]
|
||||
@property
|
||||
def emails_enabled(self) -> bool:
|
||||
return bool(self.SMTP_HOST and self.EMAILS_FROM_EMAIL)
|
||||
|
||||
# TODO: update type to EmailStr when sqlmodel supports it
|
||||
EMAIL_TEST_USER: str = "test@example.com"
|
||||
@@ -89,7 +90,6 @@ class Settings(BaseSettings):
|
||||
FIRST_SUPERUSER: str
|
||||
FIRST_SUPERUSER_PASSWORD: str
|
||||
USERS_OPEN_REGISTRATION: bool = False
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
|
||||
|
||||
settings = Settings() # type: ignore
|
||||
|
@@ -42,7 +42,8 @@ def test_recovery_password(
|
||||
client: TestClient, normal_user_token_headers: dict[str, str], mocker: MockerFixture
|
||||
) -> None:
|
||||
mocker.patch("app.utils.send_email", return_value=None)
|
||||
mocker.patch("app.core.config.settings.EMAILS_ENABLED", True)
|
||||
mocker.patch("app.core.config.settings.SMTP_HOST", "smtp.example.com")
|
||||
mocker.patch("app.core.config.settings.SMTP_USER", "admin@example.com")
|
||||
email = "test@example.com"
|
||||
r = client.post(
|
||||
f"{settings.API_V1_STR}/password-recovery/{email}",
|
||||
|
@@ -37,7 +37,8 @@ def test_create_user_new_email(
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
mocker.patch("app.utils.send_email")
|
||||
mocker.patch("app.core.config.settings.EMAILS_ENABLED", True)
|
||||
mocker.patch("app.core.config.settings.SMTP_HOST", "smtp.example.com")
|
||||
mocker.patch("app.core.config.settings.SMTP_USER", "admin@example.com")
|
||||
username = random_email()
|
||||
password = random_lower_string()
|
||||
data = {"email": username, "password": password}
|
||||
|
@@ -17,7 +17,7 @@ class EmailData:
|
||||
subject: str
|
||||
|
||||
|
||||
def render_email_template(*, template_name: str, context: dict[str, Any]):
|
||||
def render_email_template(*, template_name: str, context: dict[str, Any]) -> str:
|
||||
template_str = (Path(settings.EMAIL_TEMPLATES_DIR) / template_name).read_text()
|
||||
html_content = Template(template_str).render(context)
|
||||
return html_content
|
||||
@@ -29,7 +29,7 @@ def send_email(
|
||||
subject: str = "",
|
||||
html_content: str = "",
|
||||
) -> None:
|
||||
assert settings.EMAILS_ENABLED, "no provided configuration for email variables"
|
||||
assert settings.emails_enabled, "no provided configuration for email variables"
|
||||
message = emails.Message(
|
||||
subject=subject,
|
||||
html=html_content,
|
||||
@@ -59,8 +59,7 @@ def generate_test_email(email_to: str) -> EmailData:
|
||||
def generate_reset_password_email(email_to: str, email: str, token: str) -> EmailData:
|
||||
project_name = settings.PROJECT_NAME
|
||||
subject = f"{project_name} - Password recovery for user {email}"
|
||||
server_host = settings.SERVER_HOST
|
||||
link = f"{server_host}/reset-password?token={token}"
|
||||
link = f"{settings.server_host}/reset-password?token={token}"
|
||||
html_content = render_email_template(
|
||||
template_name="reset_password.html",
|
||||
context={
|
||||
@@ -79,7 +78,6 @@ def generate_new_account_email(
|
||||
) -> EmailData:
|
||||
project_name = settings.PROJECT_NAME
|
||||
subject = f"{project_name} - New account for user {username}"
|
||||
link = settings.SERVER_HOST
|
||||
html_content = render_email_template(
|
||||
template_name="new_account.html",
|
||||
context={
|
||||
@@ -87,7 +85,7 @@ def generate_new_account_email(
|
||||
"username": username,
|
||||
"password": password,
|
||||
"email": email_to,
|
||||
"link": link,
|
||||
"link": settings.server_host,
|
||||
},
|
||||
)
|
||||
return EmailData(html_content=html_content, subject=subject)
|
||||
|
@@ -1,10 +1,3 @@
|
||||
domain:
|
||||
type: str
|
||||
help: |
|
||||
Which domain name to use for the project, by default,
|
||||
localhost, but you should change it later (in .env)
|
||||
default: localhost
|
||||
|
||||
project_name:
|
||||
type: str
|
||||
help: The name of the project, shown to API users (in .env)
|
||||
|
173
deployment.md
173
deployment.md
@@ -1,12 +1,12 @@
|
||||
# FastAPI Project - Deployment
|
||||
|
||||
You can deploy the project using Docker Compose in a remote server.
|
||||
You can deploy the project using Docker Compose to a remote server.
|
||||
|
||||
It expects you to have a Traefik proxy handling communication to the outside world and HTTPS certificates.
|
||||
This project expects you to have a Traefik proxy handling communication to the outside world and HTTPS certificates.
|
||||
|
||||
And you can use CI (continuous integration) systems to deploy automatically.
|
||||
You can use CI/CD (continuous integration and continuous deployment) systems to deploy automatically, there are already configurations to do it with GitHub Actions.
|
||||
|
||||
But you have to configure a couple things first.
|
||||
But you have to configure a couple things first. 🤓
|
||||
|
||||
## Preparation
|
||||
|
||||
@@ -23,6 +23,8 @@ mkdir -p /root/code/fastapi-project/
|
||||
|
||||
We need a Traefik proxy to handle incoming connections and HTTPS certificates.
|
||||
|
||||
You need to do these next steps only once.
|
||||
|
||||
### Traefik Docker Compose
|
||||
|
||||
Copy the Traefik Docker Compose file to your server, to your code directory. You could do it with `rsync`:
|
||||
@@ -35,7 +37,7 @@ rsync -a docker-compose.traefik.yml root@your-server.example.com:/root/code/fast
|
||||
|
||||
This Traefik will expect a Docker "public network" named `traefik-public` to communicate with your stack(s).
|
||||
|
||||
This way, there will be a single public Traefik proxy that handles the communication (HTTP and HTTPS) with the outside world, and then behind that, you could have one or more stacks.
|
||||
This way, there will be a single public Traefik proxy that handles the communication (HTTP and HTTPS) with the outside world, and then behind that, you could have one or more stacks with different domains, even if they are on the same single server.
|
||||
|
||||
To create a Docker "public network" named `traefik-public` run:
|
||||
|
||||
@@ -47,27 +49,25 @@ docker network create traefik-public
|
||||
|
||||
The Traefik Docker Compose file expects some environment variables to be set.
|
||||
|
||||
Create the environment variables for HTTP Basic Auth.
|
||||
|
||||
* Create the username, e.g.:
|
||||
* Create the username for HTTP Basic Auth, e.g.:
|
||||
|
||||
```bash
|
||||
export USERNAME=admin
|
||||
```
|
||||
|
||||
* Create an environment variable with the password, e.g.:
|
||||
* Create an environment variable with the password for HTTP Basic Auth, e.g.:
|
||||
|
||||
```bash
|
||||
export PASSWORD=changethis
|
||||
```
|
||||
|
||||
* Use openssl to generate the "hashed" version of the password and store it in an environment variable:
|
||||
* Use openssl to generate the "hashed" version of the password for HTTP Basic Auth and store it in an environment variable:
|
||||
|
||||
```bash
|
||||
export HASHED_PASSWORD=$(openssl passwd -apr1 $PASSWORD)
|
||||
```
|
||||
|
||||
* Create an environment variable with the domain name, e.g.:
|
||||
* Create an environment variable with the domain name for your server, e.g.:
|
||||
|
||||
```bash
|
||||
export DOMAIN=fastapi-project.example.com
|
||||
@@ -79,6 +79,8 @@ export DOMAIN=fastapi-project.example.com
|
||||
export EMAIL=admin@example.com
|
||||
```
|
||||
|
||||
**Note**: you need to set a different email, an email `@example.com` won't work.
|
||||
|
||||
### Start the Traefik Docker Compose
|
||||
|
||||
Now with the environment variables set and the `docker-compose.traefik.yml` in place, you can start the Traefik Docker Compose:
|
||||
@@ -91,34 +93,161 @@ docker compose -f docker-compose.traefik.yml up -d
|
||||
|
||||
Now that you have Traefik in place you can deploy your FastAPI project with Docker Compose.
|
||||
|
||||
You could configure the variables in the `.env` file to match your domain, or you could override them before running the `docker compose` command.
|
||||
## Environment Variables
|
||||
|
||||
For example:
|
||||
You need to set some environment variables first.
|
||||
|
||||
Set the `ENVIRONMENT`, by default `local` (for development), but when deploying to a server you would put something like `staging` or `production`:
|
||||
|
||||
```bash
|
||||
export ENVIRONMENT=production
|
||||
```
|
||||
|
||||
Set the `DOMAIN`, by default `localhost` (for development), but when deploying you would use your own domain, for example:
|
||||
|
||||
```bash
|
||||
export DOMAIN=fastapi-project.example.com
|
||||
```
|
||||
|
||||
And then deploy with Docker Compose:
|
||||
You can set several variables, like:
|
||||
|
||||
* `ENVIRONMENT`: The current deployment environment, like `staging` or `production`.
|
||||
* `DOMAIN`: The current deployment domain, for example `fastapi-project.example.com`.
|
||||
* `BACKEND_CORS_ORIGINS`: A list of allowed CORS origins separated by commas.
|
||||
* `SECRET_KEY`: The secret key for the FastAPI project, used to sign tokens.
|
||||
* `FIRST_SUPERUSER`: The email of the first superuser, this superuser will be the one that can create new users.
|
||||
* `FIRST_SUPERUSER_PASSWORD`: The password of the first superuser.
|
||||
* `USERS_OPEN_REGISTRATION`: Whether to allow open registration of new users.
|
||||
* `SMTP_HOST`: The SMTP server host to send emails, this would come from your email provider (E.g. Mailgun, Sparkpost, Sendgrid, etc).
|
||||
* `SMTP_USER`: The SMTP server user to send emails.
|
||||
* `SMTP_PASSWORD`: The SMTP server password to send emails.
|
||||
* `EMAILS_FROM_EMAIL`: The email account to send emails from.
|
||||
* `POSTGRES_SERVER`: The hostname of the PostgreSQL server. You can leave the default of `db`, provided by the same Docker Compose. You normally wouldn't need to change this unless you are using a third-party provider.
|
||||
* `POSTGRES_PASSWORD`: The Postgres password.
|
||||
* `POSTGRES_USER`: The Postgres user, you can leave the default.
|
||||
* `POSTGRES_DB`: The database name to use for this application. You can leave the default of `app`.
|
||||
* `PGADMIN_DEFAULT_EMAIL`: The default email for pgAdmin.
|
||||
* `PGADMIN_DEFAULT_PASSWORD`: The default password for pgAdmin.
|
||||
* `SENTRY_DSN`: The DSN for Sentry, if you are using it.
|
||||
* `FLOWER_BASIC_AUTH`: The HTTP Basic Auth for Flower.
|
||||
|
||||
### Deploy with Docker Compose
|
||||
|
||||
With the environment variables in place, you can deploy with Docker Compose:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
For production you wouldn't want to have the overrides in `docker-compose.override.yml`, so you would need to explicitly specify the file to use, `docker-compose.yml`.
|
||||
For production you wouldn't want to have the overrides in `docker-compose.override.yml`, that's why we explicitly specify `docker-compose.yml` as the file to use.
|
||||
|
||||
## Continuous Deployment (CD)
|
||||
|
||||
You can use GitHub Actions to deploy your project automatically. 😎
|
||||
|
||||
You can have multiple environment deployments.
|
||||
|
||||
There are already two environments configured, `staging` and `production`. 🚀
|
||||
|
||||
### Install GitHub Actions Runner
|
||||
|
||||
* On your remote server, if you are running as the `root` user, create a user for your GitHub Actions:
|
||||
|
||||
```bash
|
||||
adduser github
|
||||
```
|
||||
|
||||
* Add Docker permissions to the `github` user:
|
||||
|
||||
```bash
|
||||
usermod -aG docker github
|
||||
```
|
||||
|
||||
* Temporarily switch to the `github` user:
|
||||
|
||||
```bash
|
||||
su - github
|
||||
```
|
||||
|
||||
* Go to the `github` user's home directory:
|
||||
|
||||
```bash
|
||||
cd
|
||||
```
|
||||
|
||||
* [Install a GitHub Action self-hosted runner following the official guide](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners#adding-a-self-hosted-runner-to-a-repository).
|
||||
|
||||
* When asked about labels, add a label for the environment, e.g. `production`.
|
||||
|
||||
After installing, the guide would tell you to run a command to start the runner. Nevertheless, it would stop once you terminate that process or if your local connection to your server is lost.
|
||||
|
||||
To make sure it runs on startup and continues running, you can install it as a service. To do that, exit the `github` user and go back to the `root` user:
|
||||
|
||||
```bash
|
||||
exit
|
||||
```
|
||||
|
||||
After you do it, you would be on the `root` user again. And you will be on the previous directory, belonging to the `root` user.
|
||||
|
||||
* Go to the `actions-runner` directory inside of the `github` user's home directory:
|
||||
|
||||
```bash
|
||||
cd /home/github/actions-runner
|
||||
```
|
||||
|
||||
* From there, [install the GitHub Actions runner service following the official guide](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service#installing-the-service):
|
||||
|
||||
```bash
|
||||
./svc.sh install github
|
||||
```
|
||||
|
||||
* Start the service:
|
||||
|
||||
```bash
|
||||
./svc.sh start
|
||||
```
|
||||
|
||||
### Set Secrets
|
||||
|
||||
On your repository, configure secrets for the environment variables you need, the same ones described above, including `DOMAIN`, `SECRET_KEY`, etc. Follow the [official GitHub guide for setting repository secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository).
|
||||
|
||||
## GitHub Action Deployment Workflows
|
||||
|
||||
There are GitHub Action workflows in the `.github/workflows` directory already configured for deploying to the environments (GitHub Actions runners with the labels):
|
||||
|
||||
* `staging`: after pushing (or merging) to the branch `master`.
|
||||
* `production`: after publishing a release.
|
||||
|
||||
If you need to add extra environments you could use those as starting point.
|
||||
|
||||
## URLs
|
||||
|
||||
Replace `fastapi-project.example.com` with your domain:
|
||||
Replace `fastapi-project.example.com` with your domain.
|
||||
|
||||
Frontend: https://fastapi-project.example.com
|
||||
### Main Traefik Dashboard
|
||||
|
||||
Backend API docs: https://fastapi-project.example.com/docs
|
||||
Traefik UI: `https://traefik.fastapi-project.example.com`
|
||||
|
||||
Backend API base URL: https://fastapi-project.example.com/api/
|
||||
### Production
|
||||
|
||||
PGAdmin: https://pgadmin.fastapi-project.example.com
|
||||
Frontend: `https://fastapi-project.example.com`
|
||||
|
||||
Flower: https://flower.fastapi-project.example.com
|
||||
Backend API docs: `https://fastapi-project.example.com/docs`
|
||||
|
||||
Traefik UI: https://traefik.fastapi-project.example.com
|
||||
Backend API base URL: `https://fastapi-project.example.com/api/`
|
||||
|
||||
PGAdmin: `https://pgadmin.fastapi-project.example.com`
|
||||
|
||||
Flower: `https://flower.fastapi-project.example.com`
|
||||
|
||||
### Staging
|
||||
|
||||
Frontend: `https://staging.fastapi-project.example.com`
|
||||
|
||||
Backend API docs: `https://staging.fastapi-project.example.com/docs`
|
||||
|
||||
Backend API base URL: `https://staging.fastapi-project.example.com/api/`
|
||||
|
||||
PGAdmin: `https://staging.pgadmin.fastapi-project.example.com`
|
||||
|
||||
Flower: `https://staging.flower.fastapi-project.example.com`
|
||||
|
@@ -24,6 +24,8 @@ services:
|
||||
- --accesslog
|
||||
# Enable the Traefik log, for configurations and errors
|
||||
- --log
|
||||
# Enable debug logging for local development
|
||||
- --log.level=DEBUG
|
||||
# Enable the Dashboard and API
|
||||
- --api
|
||||
# Enable the Dashboard and API in insecure mode for local development
|
||||
@@ -37,14 +39,17 @@ services:
|
||||
- traefik.http.middlewares.https-redirect.contenttype.autodetect=false
|
||||
|
||||
db:
|
||||
restart: "no"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
pgadmin:
|
||||
restart: "no"
|
||||
ports:
|
||||
- "5050:5050"
|
||||
|
||||
queue:
|
||||
restart: "no"
|
||||
ports:
|
||||
- "5671:5671"
|
||||
- "5672:5672"
|
||||
@@ -52,16 +57,16 @@ services:
|
||||
- "15671:15671"
|
||||
|
||||
flower:
|
||||
restart: "no"
|
||||
ports:
|
||||
- "5555:5555"
|
||||
|
||||
backend:
|
||||
restart: "no"
|
||||
ports:
|
||||
- "8888:8888"
|
||||
volumes:
|
||||
- ./backend/:/app
|
||||
environment:
|
||||
- SERVER_HOST=http://${DOMAIN?Variable not set}
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: backend.dockerfile
|
||||
@@ -71,11 +76,11 @@ services:
|
||||
command: /start-reload.sh
|
||||
|
||||
celeryworker:
|
||||
restart: "no"
|
||||
volumes:
|
||||
- ./backend/:/app
|
||||
environment:
|
||||
- RUN=celery worker -A app.worker -l info -Q main-queue -c 1
|
||||
- SERVER_HOST=http://${DOMAIN?Variable not set}
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: celeryworker.dockerfile
|
||||
@@ -83,6 +88,7 @@ services:
|
||||
INSTALL_DEV: ${INSTALL_DEV-true}
|
||||
|
||||
frontend:
|
||||
restart: "no"
|
||||
build:
|
||||
context: ./frontend
|
||||
args:
|
||||
|
@@ -2,15 +2,20 @@ version: "3.3"
|
||||
services:
|
||||
db:
|
||||
image: postgres:12
|
||||
restart: always
|
||||
volumes:
|
||||
- app-db-data:/var/lib/postgresql/data/pgdata
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set}
|
||||
- POSTGRES_USER=${POSTGRES_USER?Variable not set}
|
||||
- POSTGRES_DB=${POSTGRES_DB?Variable not set}
|
||||
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4
|
||||
restart: always
|
||||
networks:
|
||||
- traefik-public
|
||||
- default
|
||||
@@ -18,6 +23,9 @@ services:
|
||||
- db
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL}
|
||||
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-public
|
||||
@@ -37,14 +45,18 @@ services:
|
||||
# image: rabbitmq:3-management
|
||||
#
|
||||
# You also have to change the flower command
|
||||
restart: always
|
||||
|
||||
flower:
|
||||
image: mher/flower:0.9.7
|
||||
restart: always
|
||||
networks:
|
||||
- traefik-public
|
||||
- default
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- FLOWER_BASIC_AUTH=${FLOWER_BASIC_AUTH}
|
||||
command:
|
||||
- "--broker=amqp://guest@queue:5672//"
|
||||
# For the "Broker" tab to work in the flower UI, uncomment the following command argument,
|
||||
@@ -65,6 +77,7 @@ services:
|
||||
|
||||
backend:
|
||||
image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}'
|
||||
restart: always
|
||||
networks:
|
||||
- traefik-public
|
||||
- default
|
||||
@@ -73,10 +86,23 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- SERVER_NAME=${DOMAIN?Variable not set}
|
||||
- SERVER_HOST=https://${DOMAIN?Variable not set}
|
||||
# Allow explicit env var override for tests
|
||||
- DOMAIN=${DOMAIN}
|
||||
- ENVIRONMENT=${ENVIRONMENT}
|
||||
- BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS}
|
||||
- SECRET_KEY=${SECRET_KEY?Variable not set}
|
||||
- FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set}
|
||||
- FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set}
|
||||
- USERS_OPEN_REGISTRATION=${USERS_OPEN_REGISTRATION}
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_USER=${SMTP_USER}
|
||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||
- EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL}
|
||||
- POSTGRES_SERVER=db
|
||||
- POSTGRES_DB=${POSTGRES_DB}
|
||||
- POSTGRES_USER=${POSTGRES_USER?Variable not set}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set}
|
||||
- SENTRY_DSN=${SENTRY_DSN}
|
||||
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: backend.dockerfile
|
||||
@@ -101,15 +127,29 @@ services:
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.middlewares=${STACK_NAME?Variable not set}-www-redirect
|
||||
celeryworker:
|
||||
image: '${DOCKER_IMAGE_CELERYWORKER?Variable not set}:${TAG-latest}'
|
||||
restart: always
|
||||
depends_on:
|
||||
- db
|
||||
- queue
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- SERVER_HOST=https://${DOMAIN?Variable not set}
|
||||
# Allow explicit env var override for tests
|
||||
- SMTP_HOST=${SMTP_HOST?Variable not set}
|
||||
- DOMAIN=${DOMAIN}
|
||||
- ENVIRONMENT=${ENVIRONMENT}
|
||||
- BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS}
|
||||
- SECRET_KEY=${SECRET_KEY?Variable not set}
|
||||
- FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set}
|
||||
- FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set}
|
||||
- USERS_OPEN_REGISTRATION=${USERS_OPEN_REGISTRATION}
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_USER=${SMTP_USER}
|
||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||
- EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL}
|
||||
- POSTGRES_SERVER=db
|
||||
- POSTGRES_DB=${POSTGRES_DB}
|
||||
- POSTGRES_USER=${POSTGRES_USER?Variable not set}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set}
|
||||
- SENTRY_DSN=${SENTRY_DSN}
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: celeryworker.dockerfile
|
||||
@@ -118,6 +158,7 @@ services:
|
||||
|
||||
frontend:
|
||||
image: '${DOCKER_IMAGE_FRONTEND?Variable not set}:${TAG-latest}'
|
||||
restart: always
|
||||
networks:
|
||||
- traefik-public
|
||||
- default
|
||||
@@ -147,9 +188,6 @@ services:
|
||||
# Redirect a domain with www to non-www
|
||||
# To disable it remove the next line
|
||||
- traefik.http.middlewares.${STACK_NAME?Variable not set}-www-redirect.redirectregex.replacement=http$${1}://${DOMAIN?Variable not set}/$${3}
|
||||
# Redirect a domain without www to www
|
||||
# To enable it remove the previous line and uncomment the next
|
||||
# - traefik.http.middlewares.${STACK_NAME}-www-redirect.redirectregex.replacement=https://www.${DOMAIN}/$${3}
|
||||
# Middleware to redirect www, to disable it remove the next line
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.middlewares=${STACK_NAME?Variable not set}-www-redirect
|
||||
# Middleware to redirect www, and redirect HTTP to HTTPS
|
||||
|
Reference in New Issue
Block a user