Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0d5defa97 | ||
|
|
dae9532ef6 | ||
|
|
e316bc92a5 | ||
|
|
3878837d25 | ||
|
|
b1db7c274f | ||
|
|
ac0ccf47e5 | ||
|
|
1f416dbe60 | ||
|
|
9afa7da9c4 | ||
|
|
aac037676c | ||
|
|
321e4b6b35 |
@@ -1,10 +1,10 @@
|
|||||||
# backend-python
|
# backend-python
|
||||||
|
|
||||||
Copier template for a `backend/` part: a uv project with one Docker image and one `python -m <module>` entrypoint per service (`api`, `bot`, `userbot`, `worker`, `scheduler`), pydantic-settings, rich logging, optional Dishka, MongoDB or PostgreSQL, Redis, taskiq, CryptoPay, pydantic-ai and a DynamicConfig stored in the database.
|
Copier template for a Python backend, either as a `backend/` part next to other parts or alone at the root of the project (`layout`): a uv project with one Docker image and one `python -m <module>` entrypoint per service (`api`, `bot`, `userbot`, `worker`, `scheduler`), pydantic-settings, rich logging, optional Dishka, MongoDB or PostgreSQL, Redis, taskiq (in-memory, Redis or PostgreSQL broker), CryptoPay, pydantic-ai and a DynamicConfig stored in the database.
|
||||||
|
|
||||||
Ships `backend/docker-compose.yml` with root-relative paths, `docker-compose.local.yml` (published ports) and `docker-compose.prod.yml` (the `caddy` network), a `Makefile` with `fmt`, `check`, `build` and service-specific targets, `.env.example` and `CLAUDE.md`. Meant to be applied on top of the `infra` template.
|
Ships `backend/docker-compose.yml` with root-relative paths and profiles per service, `docker-compose.override.yml.example` (published ports and the `caddy` network aliases, copied by `make env`), a `Makefile` with `fmt`, `check`, `build` and service-specific targets, `.env.example` and `CLAUDE.md`. Applied on top of the `infra` template, `layout: parts` or `layout: single` respectively.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
copier copy --trust -d project_name="My Thing" <this-repo> <dest>
|
copier copy --trust -d project_name="My Thing" <this-repo> <dest>
|
||||||
copier update -a .copier-answers.backend.yml
|
copier update -a .copier/backend.yml
|
||||||
```
|
```
|
||||||
|
|||||||
+25
-4
@@ -1,6 +1,6 @@
|
|||||||
_subdirectory: template
|
_subdirectory: template
|
||||||
_templates_suffix: .jinja
|
_templates_suffix: .jinja
|
||||||
_answers_file: .copier-answers.backend.yml
|
_answers_file: .copier/backend.yml
|
||||||
_envops:
|
_envops:
|
||||||
keep_trailing_newline: true
|
keep_trailing_newline: true
|
||||||
|
|
||||||
@@ -25,6 +25,14 @@ author_email:
|
|||||||
type: str
|
type: str
|
||||||
default: ""
|
default: ""
|
||||||
|
|
||||||
|
layout:
|
||||||
|
type: str
|
||||||
|
help: Where the backend lives
|
||||||
|
choices:
|
||||||
|
In backend/ next to other parts: part
|
||||||
|
At the root of the project: root
|
||||||
|
default: part
|
||||||
|
|
||||||
backend_services:
|
backend_services:
|
||||||
type: str
|
type: str
|
||||||
multiselect: true
|
multiselect: true
|
||||||
@@ -63,6 +71,9 @@ taskiq_broker:
|
|||||||
choices:
|
choices:
|
||||||
in-memory: memory
|
in-memory: memory
|
||||||
Redis: redis
|
Redis: redis
|
||||||
|
PostgreSQL (taskiq-pg):
|
||||||
|
value: postgres
|
||||||
|
validator: "{% if database != 'postgres' %}Requires the PostgreSQL database{% endif %}"
|
||||||
default: memory
|
default: memory
|
||||||
|
|
||||||
include_payments:
|
include_payments:
|
||||||
@@ -84,7 +95,7 @@ dynamic_config:
|
|||||||
|
|
||||||
serve_spa:
|
serve_spa:
|
||||||
type: bool
|
type: bool
|
||||||
when: "{{ 'api' in backend_services }}"
|
when: "{{ layout == 'part' and 'api' in backend_services }}"
|
||||||
help: Serve the frontend build from the api as a SPA
|
help: Serve the frontend build from the api as a SPA
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
@@ -93,6 +104,16 @@ install_deps:
|
|||||||
help: uv sync after generation
|
help: uv sync after generation
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
|
part_dir:
|
||||||
|
type: str
|
||||||
|
when: false
|
||||||
|
default: "{{ 'backend' if layout == 'part' else '.' }}"
|
||||||
|
|
||||||
|
prefix:
|
||||||
|
type: str
|
||||||
|
when: false
|
||||||
|
default: "{{ 'backend/' if layout == 'part' else '' }}"
|
||||||
|
|
||||||
_tasks:
|
_tasks:
|
||||||
- "{% if _copier_operation == 'copy' and install_deps %}sh -c 'cd backend && uv sync'{% else %}true{% endif %}"
|
- "{% if _copier_operation == 'copy' and install_deps %}sh -c 'cd {{ part_dir }} && uv sync'{% else %}true{% endif %}"
|
||||||
- "{% if install_deps %}sh -c 'cd backend && uv run ruff format -q'{% else %}true{% endif %}"
|
- "{% if install_deps %}sh -c 'cd {{ part_dir }} && uv run ruff format -q'{% else %}true{% endif %}"
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
RUN_ENVIRONMENT=dev
|
|
||||||
LOG__LEVEL=INFO
|
|
||||||
LOG__LEVEL_EXTERNAL=WARNING
|
|
||||||
LOG__SHOW_TIME=false
|
|
||||||
LOG__CONSOLE_WIDTH=150
|
|
||||||
{% if database == 'mongo' %}DB__HOST=mongodb
|
|
||||||
DB__PORT=27017
|
|
||||||
DB__USER={{ project_slug }}
|
|
||||||
DB__PASSWORD={{ project_slug }}
|
|
||||||
DB__DB_NAME={{ project_slug }}
|
|
||||||
{% endif %}{% if database == 'postgres' %}DB__HOST=postgres
|
|
||||||
DB__PORT=5432
|
|
||||||
DB__USER={{ project_slug }}
|
|
||||||
DB__PASSWORD={{ project_slug }}
|
|
||||||
DB__DB_NAME={{ project_slug }}
|
|
||||||
{% endif %}{% if use_redis %}REDIS__HOST=redis
|
|
||||||
REDIS__PORT=6379
|
|
||||||
REDIS__CACHE_TTL=300
|
|
||||||
{% endif %}{% if 'api' in backend_services %}API__HOST=0.0.0.0
|
|
||||||
API__PORT=8080
|
|
||||||
API__WORKERS=1
|
|
||||||
{% endif %}{% if 'aiogram_bot' in backend_services %}BOT__TOKEN=
|
|
||||||
{% endif %}{% if include_payments %}CRYPTO_PAY__TOKEN=
|
|
||||||
{% endif %}{% if use_llm %}LLM__MODEL=google-gla:gemini-2.5-flash
|
|
||||||
LLM__GEMINI_API_KEY=
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{%- set svc_modules = [] -%}
|
|
||||||
{%- if 'api' in backend_services %}{% set _ = svc_modules.append("api") %}{% endif -%}
|
|
||||||
{%- if 'aiogram_bot' in backend_services %}{% set _ = svc_modules.append("bot") %}{% endif -%}
|
|
||||||
{%- if 'taskiq_worker' in backend_services %}{% set _ = svc_modules.append("worker") %}{% endif -%}
|
|
||||||
{%- if 'kurigram_userbot' in backend_services %}{% set _ = svc_modules.append("userbot") %}{% endif -%}
|
|
||||||
{%- if 'scheduler' in backend_services and 'taskiq_worker' not in backend_services %}{% set _ = svc_modules.append("scheduler") %}{% endif -%}
|
|
||||||
{%- set has_script = dynamic_config and svc_modules -%}
|
|
||||||
COMPOSE := docker compose --project-directory ..
|
|
||||||
|
|
||||||
.PHONY: fmt check build{% if database == 'postgres' %} migrate revision{% endif %}{% if has_script %} script{% endif %}{% if 'kurigram_userbot' in backend_services %} session{% endif %}
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
uv run ruff format
|
|
||||||
uv run ruff check --fix
|
|
||||||
|
|
||||||
check:
|
|
||||||
uv run ruff format --check
|
|
||||||
uv run ruff check
|
|
||||||
uv run ty check
|
|
||||||
|
|
||||||
build:
|
|
||||||
$(COMPOSE) build {{ svc_modules[0] }}
|
|
||||||
{% if database == 'postgres' %}
|
|
||||||
migrate:
|
|
||||||
$(COMPOSE) --profile external run --rm migrator
|
|
||||||
|
|
||||||
revision:
|
|
||||||
$(COMPOSE) --profile external run --rm migrator revision --autogenerate -m "$(m)"
|
|
||||||
{% endif %}{% if has_script %}
|
|
||||||
script:
|
|
||||||
@$(COMPOSE) run --rm {{ svc_modules[0] }} scripts.$(word 2,$(MAKECMDGOALS)) $(wordlist 3,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
|
||||||
|
|
||||||
%:
|
|
||||||
@:
|
|
||||||
{% endif %}{% if 'kurigram_userbot' in backend_services %}
|
|
||||||
session:
|
|
||||||
uv run python scripts/session/create.py
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{%- set has_ports = 'api' in backend_services or database != 'none' or use_redis -%}
|
|
||||||
services:{% if not has_ports %} {}{% endif %}
|
|
||||||
{% if 'api' in backend_services %} api:
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:${API__PORT:-8080}:8080"
|
|
||||||
{% endif %}{% if database == 'mongo' %} mongodb:
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:${DB__PORT:-27017}:${DB__PORT:-27017}"
|
|
||||||
{% endif %}{% if database == 'postgres' %} postgres:
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:${DB__PORT:-5432}:5432"
|
|
||||||
{% endif %}{% if use_redis %} redis:
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:${REDIS__PORT:-6379}:6379"
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{%- set dash = project_slug | replace('_', '-') -%}
|
|
||||||
{% if 'api' in backend_services -%}
|
|
||||||
services:
|
|
||||||
api:
|
|
||||||
networks:
|
|
||||||
caddy:
|
|
||||||
aliases:
|
|
||||||
- {{ dash }}-api
|
|
||||||
|
|
||||||
networks:
|
|
||||||
caddy:
|
|
||||||
external: true
|
|
||||||
{%- else -%}
|
|
||||||
services: {}
|
|
||||||
{%- endif %}
|
|
||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
import uvicorn
|
|
||||||
|
|
||||||
from utils.env import env
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
uvicorn.run(
|
|
||||||
"api.app:app", host=env.api.host, port=env.api.port, workers=env.api.workers
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
from .modules.client import PyroClient
|
|
||||||
|
|
||||||
__all__ = ["PyroClient"]
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
[environment]
|
|
||||||
python = "backend/.venv"
|
|
||||||
|
|
||||||
[src]
|
|
||||||
exclude = ["backend/migrations"]
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[environment]
|
||||||
|
python = "{{ prefix }}.venv"
|
||||||
|
|
||||||
|
[src]
|
||||||
|
exclude = ["{{ prefix }}migrations"]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{%- set groups = [] -%}
|
||||||
|
{%- if layout == 'root' %}{% set _ = groups.append(["COMPOSE_PROFILES=" ~ ("external," if database != 'none' or use_redis else "") ~ "services"]) %}{% endif -%}
|
||||||
|
{%- set _ = groups.append(["RUN_ENVIRONMENT=prod", "TZ=UTC"]) -%}
|
||||||
|
{%- set _ = groups.append(["LOG__LEVEL=INFO", "LOG__LEVEL_EXTERNAL=WARNING", "LOG__SHOW_TIME=false", "LOG__CONSOLE_WIDTH=150"]) -%}
|
||||||
|
{%- if database == 'mongo' %}{% set _ = groups.append(["DB__HOST=mongodb", "DB__PORT=27017", "DB__USER=" ~ project_slug, "DB__PASSWORD=" ~ project_slug, "DB__DB_NAME=" ~ project_slug]) %}{% endif -%}
|
||||||
|
{%- if database == 'postgres' %}{% set _ = groups.append(["DB__HOST=postgres", "DB__PORT=5432", "DB__USER=" ~ project_slug, "DB__PASSWORD=" ~ project_slug, "DB__DB_NAME=" ~ project_slug]) %}{% endif -%}
|
||||||
|
{%- if use_redis %}{% set _ = groups.append(["REDIS__HOST=redis", "REDIS__PORT=6379", "REDIS__CACHE_TTL=300"]) %}{% endif -%}
|
||||||
|
{%- if 'api' in backend_services %}{% set _ = groups.append(["API__HOST=0.0.0.0", "API__PORT=8080", "API__WORKERS=1", "API__DOCS=false"]) %}{% endif -%}
|
||||||
|
{%- if 'aiogram_bot' in backend_services %}{% set _ = groups.append(["BOT__TOKEN="]) %}{% endif -%}
|
||||||
|
{%- if include_payments %}{% set _ = groups.append(["CRYPTO_PAY__TOKEN="]) %}{% endif -%}
|
||||||
|
{%- if use_llm %}{% set _ = groups.append(["LLM__MODEL=google-gla:gemini-2.5-flash", "LLM__GEMINI_API_KEY="]) %}{% endif -%}
|
||||||
|
{% for group in groups %}{{ group | join("\n") }}
|
||||||
|
{% if not loop.last %}
|
||||||
|
{% endif %}{% endfor %}
|
||||||
@@ -10,6 +10,8 @@ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
|||||||
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
FROM ghcr.io/astral-sh/uv:python3.13-alpine
|
FROM ghcr.io/astral-sh/uv:python3.13-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache tzdata
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
{%- set svc_modules = [] -%}
|
||||||
|
{%- if 'api' in backend_services %}{% set _ = svc_modules.append("api") %}{% endif -%}
|
||||||
|
{%- if 'aiogram_bot' in backend_services %}{% set _ = svc_modules.append("bot") %}{% endif -%}
|
||||||
|
{%- if 'taskiq_worker' in backend_services %}{% set _ = svc_modules.append("worker") %}{% endif -%}
|
||||||
|
{%- if 'kurigram_userbot' in backend_services %}{% set _ = svc_modules.append("userbot") %}{% endif -%}
|
||||||
|
{%- if 'scheduler' in backend_services and 'taskiq_worker' not in backend_services %}{% set _ = svc_modules.append("scheduler") %}{% endif -%}
|
||||||
|
{%- set app_services = [] -%}
|
||||||
|
{%- if 'api' in backend_services %}{% set _ = app_services.append("api") %}{% endif -%}
|
||||||
|
{%- if 'aiogram_bot' in backend_services %}{% set _ = app_services.append("bot") %}{% endif -%}
|
||||||
|
{%- if 'taskiq_worker' in backend_services %}{% set _ = app_services.append("worker") %}{% endif -%}
|
||||||
|
{%- if 'kurigram_userbot' in backend_services %}{% set _ = app_services.append("userbot") %}{% endif -%}
|
||||||
|
{%- if 'scheduler' in backend_services %}{% set _ = app_services.append("scheduler") %}{% endif -%}
|
||||||
|
{%- set has_script = dynamic_config and svc_modules -%}
|
||||||
|
{%- set has_scripts = dynamic_config or 'kurigram_userbot' in backend_services -%}
|
||||||
|
COMPOSE := {% if layout == 'part' %}cd .. && {% endif %}docker compose
|
||||||
|
{% if svc_modules %}IMAGE := {{ project_slug }}/backend
|
||||||
|
IMAGE_INPUTS := pyproject.toml uv.lock Dockerfile
|
||||||
|
CODE := src{% if has_scripts %} scripts{% endif %}{% if serve_spa %} ../frontend{% endif %}
|
||||||
|
SERVICES := {{ app_services | join(' ') }}
|
||||||
|
changed = ! git diff --quiet $(1) -- $(2) 2>/dev/null
|
||||||
|
stamp = { git update-ref $(1) HEAD 2>/dev/null || true; }
|
||||||
|
{% endif %}
|
||||||
|
.PHONY: {% if layout == 'root' %}env recreate rebuild restart logs down deploy {% endif %}fmt check{% if svc_modules %} build pre-deploy post-deploy{% endif %}{% if database == 'postgres' %} migrate revision{% endif %}{% if has_script %} script{% endif %}{% if 'kurigram_userbot' in backend_services %} session{% endif %}
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
uv run ruff format
|
||||||
|
uv run ruff check --fix
|
||||||
|
|
||||||
|
check:
|
||||||
|
uv run ruff format --check
|
||||||
|
uv run ruff check
|
||||||
|
uv run ty check
|
||||||
|
{% if svc_modules %}
|
||||||
|
build:
|
||||||
|
$(COMPOSE) build {{ svc_modules[0] }}
|
||||||
|
|
||||||
|
pre-deploy:
|
||||||
|
@if $(call changed,refs/deploy/backend,$(IMAGE_INPUTS)) || ! docker image inspect $(IMAGE) >/dev/null 2>&1; then \
|
||||||
|
$(MAKE) build && $(call stamp,refs/deploy/backend) && $(call stamp,refs/deploy/backend-code); \
|
||||||
|
fi
|
||||||
|
{% if database == 'postgres' %} @$(COMPOSE) config --services | grep -qx postgres && $(COMPOSE) up -d postgres || true
|
||||||
|
$(MAKE) migrate
|
||||||
|
{% endif %}
|
||||||
|
post-deploy:
|
||||||
|
@if $(call changed,refs/deploy/backend-code,$(CODE)); then \
|
||||||
|
running=$$($(COMPOSE) ps --services --status running | grep -x $(addprefix -e ,$(SERVICES))); \
|
||||||
|
[ -z "$$running" ] || $(COMPOSE) restart $$running; \
|
||||||
|
$(call stamp,refs/deploy/backend-code); \
|
||||||
|
fi
|
||||||
|
{% endif %}{% if layout == 'root' %}
|
||||||
|
env:
|
||||||
|
@test -f .env || cp .env.example .env
|
||||||
|
@test -f docker-compose.override.yml || cp docker-compose.override.yml.example docker-compose.override.yml
|
||||||
|
|
||||||
|
recreate:
|
||||||
|
$(COMPOSE) up -d --force-recreate
|
||||||
|
|
||||||
|
rebuild: build
|
||||||
|
$(COMPOSE) up -d
|
||||||
|
|
||||||
|
restart:
|
||||||
|
$(COMPOSE) restart
|
||||||
|
|
||||||
|
logs:
|
||||||
|
$(COMPOSE) logs -f --tail=100
|
||||||
|
|
||||||
|
down:
|
||||||
|
$(COMPOSE) down
|
||||||
|
{% if svc_modules %}
|
||||||
|
deploy: pre-deploy
|
||||||
|
$(COMPOSE) up -d
|
||||||
|
$(MAKE) post-deploy
|
||||||
|
{% endif %}{% endif %}{% if database == 'postgres' %}
|
||||||
|
migrate:
|
||||||
|
$(COMPOSE) run --rm migrator $(or $(filter-out $@,$(MAKECMDGOALS)),upgrade head)
|
||||||
|
|
||||||
|
revision:
|
||||||
|
$(COMPOSE) run --rm migrator revision --autogenerate -m "$(m)"
|
||||||
|
{% endif %}{% if has_script %}
|
||||||
|
script:
|
||||||
|
@$(COMPOSE) run --rm {{ svc_modules[0] }} scripts.$(word 2,$(MAKECMDGOALS)) $(wordlist 3,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||||
|
{% endif %}{% if database == 'postgres' or has_script %}
|
||||||
|
%:
|
||||||
|
@:
|
||||||
|
{% endif %}{% if 'kurigram_userbot' in backend_services %}
|
||||||
|
session:
|
||||||
|
uv run python scripts/session/create.py
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{%- set dash = project_slug | replace('_', '-') -%}
|
||||||
|
{%- set entries = [] -%}
|
||||||
|
{%- if 'api' in backend_services %}{% set _ = entries.append('api') %}{% endif -%}
|
||||||
|
{%- if database == 'mongo' %}{% set _ = entries.append('mongodb') %}{% endif -%}
|
||||||
|
{%- if database == 'postgres' %}{% set _ = entries.append('postgres') %}{% endif -%}
|
||||||
|
{%- if use_redis %}{% set _ = entries.append('redis') %}{% endif -%}
|
||||||
|
services:{% if not entries %} {}{% endif %}
|
||||||
|
{% for entry in entries %}{% if entry == 'api' %} api:
|
||||||
|
networks:
|
||||||
|
default: {}
|
||||||
|
caddy:
|
||||||
|
aliases:
|
||||||
|
- {{ dash }}-api
|
||||||
|
{% elif entry == 'mongodb' %} mongodb:
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${DB__PORT:-27017}:${DB__PORT:-27017}"
|
||||||
|
{% elif entry == 'postgres' %} postgres:
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${DB__PORT:-5432}:5432"
|
||||||
|
{% elif entry == 'redis' %} redis:
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${REDIS__PORT:-6379}:6379"
|
||||||
|
{% endif %}{% if not loop.last %}
|
||||||
|
{% endif %}{% endfor %}{% if 'api' in backend_services %}
|
||||||
|
networks:
|
||||||
|
caddy:
|
||||||
|
external: true
|
||||||
|
{% endif %}
|
||||||
+35
-14
@@ -2,6 +2,10 @@
|
|||||||
{%- set svc_networks = [] -%}
|
{%- set svc_networks = [] -%}
|
||||||
{%- if database != 'none' %}{% set _ = svc_networks.append('database') %}{% endif -%}
|
{%- if database != 'none' %}{% set _ = svc_networks.append('database') %}{% endif -%}
|
||||||
{%- if use_redis %}{% set _ = svc_networks.append('redis') %}{% endif -%}
|
{%- if use_redis %}{% set _ = svc_networks.append('redis') %}{% endif -%}
|
||||||
|
{%- set external_services = [] -%}
|
||||||
|
{%- if database == 'mongo' %}{% set _ = external_services.append('mongodb') %}{% endif -%}
|
||||||
|
{%- if database == 'postgres' %}{% set _ = external_services.append('postgres') %}{% endif -%}
|
||||||
|
{%- if use_redis %}{% set _ = external_services.append('redis') %}{% endif -%}
|
||||||
{%- set app_services = [] -%}
|
{%- set app_services = [] -%}
|
||||||
{%- if 'api' in backend_services %}{% set _ = app_services.append({'name': 'api', 'command': '[api]'}) %}{% endif -%}
|
{%- if 'api' in backend_services %}{% set _ = app_services.append({'name': 'api', 'command': '[api]'}) %}{% endif -%}
|
||||||
{%- if 'aiogram_bot' in backend_services %}{% set _ = app_services.append({'name': 'bot', 'command': '[bot]'}) %}{% endif -%}
|
{%- if 'aiogram_bot' in backend_services %}{% set _ = app_services.append({'name': 'bot', 'command': '[bot]'}) %}{% endif -%}
|
||||||
@@ -9,25 +13,31 @@
|
|||||||
{%- if 'kurigram_userbot' in backend_services %}{% set _ = app_services.append({'name': 'userbot', 'command': '[userbot]'}) %}{% endif -%}
|
{%- if 'kurigram_userbot' in backend_services %}{% set _ = app_services.append({'name': 'userbot', 'command': '[userbot]'}) %}{% endif -%}
|
||||||
{%- if 'scheduler' in backend_services %}{% set _ = app_services.append({'name': 'scheduler', 'command': ('[worker, scheduler]' if 'taskiq_worker' in backend_services else '[scheduler]')}) %}{% endif -%}
|
{%- if 'scheduler' in backend_services %}{% set _ = app_services.append({'name': 'scheduler', 'command': ('[worker, scheduler]' if 'taskiq_worker' in backend_services else '[scheduler]')}) %}{% endif -%}
|
||||||
{%- macro backend_service(name, command) %} {{ name }}:
|
{%- macro backend_service(name, command) %} {{ name }}:
|
||||||
build: backend
|
build: {{ part_dir }}
|
||||||
image: {{ project_slug }}/backend
|
image: {{ project_slug }}/backend
|
||||||
profiles: [{{ name }}, services]
|
profiles: [{{ name }}, services]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- path: .env
|
- path: .env
|
||||||
required: false
|
required: false
|
||||||
- path: backend/.env
|
{% if layout == 'part' %} - path: backend/.env
|
||||||
required: false
|
required: false
|
||||||
environment:
|
{% endif %} environment:
|
||||||
RUN_ENVIRONMENT: prod
|
RUN_ENVIRONMENT: prod
|
||||||
|
TZ: ${TZ:-UTC}
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend/src:/app/src
|
- ./{{ prefix }}src:/app/src
|
||||||
{% if has_scripts %} - ./backend/scripts:/app/scripts
|
{% if has_scripts %} - ./{{ prefix }}scripts:/app/scripts
|
||||||
{% endif %}{% if name == 'userbot' %} - ./backend/sessions:/app/sessions
|
{% endif %}{% if name == 'userbot' %} - ./{{ prefix }}sessions:/app/sessions
|
||||||
{% endif %}{% if name == 'api' and serve_spa %} - ./frontend/build:/app/static:ro
|
{% endif %}{% if name == 'api' and serve_spa %} - ./frontend/build:/app/static:ro
|
||||||
{% endif %} command: {{ command }}
|
{% endif %} command: {{ command }}
|
||||||
{% if svc_networks %} networks:
|
{% if external_services %} depends_on:
|
||||||
{% for net in svc_networks %} {{ net }}:
|
{% for ext in external_services %} {{ ext }}:
|
||||||
|
condition: service_healthy
|
||||||
|
required: false
|
||||||
|
{% endfor %}{% endif %}{% if svc_networks %} networks:
|
||||||
|
{% if name == 'api' %} default:
|
||||||
|
{% endif %}{% for net in svc_networks %} {{ net }}:
|
||||||
{% endfor %}{% endif %}{% endmacro -%}
|
{% endfor %}{% endif %}{% endmacro -%}
|
||||||
services:
|
services:
|
||||||
{% for svc in app_services %}{{ backend_service(svc.name, svc.command) }}{% if not loop.last %}
|
{% for svc in app_services %}{{ backend_service(svc.name, svc.command) }}{% if not loop.last %}
|
||||||
@@ -43,6 +53,11 @@ services:
|
|||||||
command: mongod --port ${DB__PORT:-27017}
|
command: mongod --port ${DB__PORT:-27017}
|
||||||
volumes:
|
volumes:
|
||||||
- database:/data/db
|
- database:/data/db
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--quiet", "--port", "${DB__PORT:-27017}", "--eval", "db.adminCommand('ping')"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
networks:
|
networks:
|
||||||
database:
|
database:
|
||||||
aliases:
|
aliases:
|
||||||
@@ -70,25 +85,26 @@ services:
|
|||||||
- ${DB__HOST:-postgres}
|
- ${DB__HOST:-postgres}
|
||||||
|
|
||||||
migrator:
|
migrator:
|
||||||
build: backend
|
build: {{ part_dir }}
|
||||||
image: {{ project_slug }}/backend
|
image: {{ project_slug }}/backend
|
||||||
profiles: [migrate]
|
profiles: [migrate]
|
||||||
env_file:
|
env_file:
|
||||||
- path: .env
|
- path: .env
|
||||||
required: false
|
required: false
|
||||||
- path: backend/.env
|
{% if layout == 'part' %} - path: backend/.env
|
||||||
required: false
|
required: false
|
||||||
environment:
|
{% endif %} environment:
|
||||||
RUN_ENVIRONMENT: prod
|
RUN_ENVIRONMENT: prod
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend/src:/app/src
|
- ./{{ prefix }}src:/app/src
|
||||||
- ./backend/migrations:/app/migrations
|
- ./{{ prefix }}migrations:/app/migrations
|
||||||
- ./backend/alembic.ini:/app/alembic.ini
|
- ./{{ prefix }}alembic.ini:/app/alembic.ini
|
||||||
entrypoint: [alembic]
|
entrypoint: [alembic]
|
||||||
command: [upgrade, head]
|
command: [upgrade, head]
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
required: false
|
||||||
networks:
|
networks:
|
||||||
database:
|
database:
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -98,6 +114,11 @@ services:
|
|||||||
profiles: [redis, external]
|
profiles: [redis, external]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: ["redis-server", "--appendonly", "yes"]
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
networks:
|
networks:
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
{%- if 'aiogram_bot' in backend_services %}{% set _ = deps.append("aiogram>=3.29.0") %}{% endif -%}
|
{%- if 'aiogram_bot' in backend_services %}{% set _ = deps.append("aiogram>=3.29.0") %}{% endif -%}
|
||||||
{%- if include_payments %}{% set _ = deps.append("aiosend>=3.0.7") %}{% endif -%}
|
{%- if include_payments %}{% set _ = deps.append("aiosend>=3.0.7") %}{% endif -%}
|
||||||
{%- if 'kurigram_userbot' in backend_services %}{% set _ = deps.append("anyio>=4.0.0") %}{% set _ = deps.append("kurigram>=2.2.7") %}{% set _ = deps.append("tgcrypto>=1.2.5") %}{% set _ = deps.append("uvloop>=0.21.0") %}{% endif -%}
|
{%- if 'kurigram_userbot' in backend_services %}{% set _ = deps.append("anyio>=4.0.0") %}{% set _ = deps.append("kurigram>=2.2.7") %}{% set _ = deps.append("tgcrypto>=1.2.5") %}{% set _ = deps.append("uvloop>=0.21.0") %}{% endif -%}
|
||||||
{%- if 'taskiq_worker' in backend_services %}{% set _ = deps.append("taskiq>=0.12.4") %}{% if taskiq_broker == 'redis' %}{% set _ = deps.append("taskiq-redis>=1.2.3") %}{% endif %}{% endif -%}
|
{%- if 'taskiq_worker' in backend_services %}{% set _ = deps.append("taskiq>=0.12.4") %}{% if taskiq_broker == 'redis' %}{% set _ = deps.append("taskiq-redis>=1.2.3") %}{% endif %}{% if taskiq_broker == 'postgres' %}{% set _ = deps.append("taskiq-pg>=0.2.0") %}{% endif %}{% endif -%}
|
||||||
{%- if 'scheduler' in backend_services %}{% set _ = deps.append("apscheduler>=3.11.0") %}{% endif -%}
|
{%- if 'scheduler' in backend_services %}{% set _ = deps.append("apscheduler>=3.11.0") %}{% endif -%}
|
||||||
{%- if database == 'mongo' %}{% set _ = deps.append("beanie>=2.0.1") %}{% endif -%}
|
{%- if database == 'mongo' %}{% set _ = deps.append("beanie>=2.0.1") %}{% endif -%}
|
||||||
{%- if database == 'postgres' %}{% set _ = deps.append("sqlmodel>=0.0.38") %}{% set _ = deps.append("asyncpg>=0.31.0") %}{% set _ = deps.append("alembic>=1.18.4") %}{% set _ = deps.append("greenlet>=3.1.0") %}{% endif -%}
|
{%- if database == 'postgres' %}{% set _ = deps.append("sqlmodel>=0.0.38") %}{% set _ = deps.append("asyncpg>=0.31.0") %}{% set _ = deps.append("alembic>=1.18.4") %}{% set _ = deps.append("greenlet>=3.1.0") %}{% endif -%}
|
||||||
+5
-1
@@ -69,6 +69,7 @@ class ApiSettings(BaseSettings):
|
|||||||
host: str = "0.0.0.0" # noqa: S104
|
host: str = "0.0.0.0" # noqa: S104
|
||||||
port: int = 8080
|
port: int = 8080
|
||||||
workers: int = 1
|
workers: int = 1
|
||||||
|
docs: bool = False
|
||||||
{%- if serve_spa %}
|
{%- if serve_spa %}
|
||||||
static_dir: str = "static"
|
static_dir: str = "static"
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
@@ -110,7 +111,10 @@ class Settings(BaseSettings):
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
case_sensitive=False, env_file=".env", env_nested_delimiter="__", extra="ignore"
|
case_sensitive=False,
|
||||||
|
env_file={% if layout == 'part' %}("../.env", ".env"){% else %}".env"{% endif %},
|
||||||
|
env_nested_delimiter="__",
|
||||||
|
extra="ignore",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from utils.env import env
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
uvicorn.run(
|
||||||
|
"api.app:app",
|
||||||
|
host=env.api.host,
|
||||||
|
port=env.api.port,
|
||||||
|
workers=env.api.workers,
|
||||||
|
forwarded_allow_ips="*",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+7
-3
@@ -10,8 +10,8 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from api import routers
|
from api import routers
|
||||||
{% if use_dishka %}from dependencies.container import container
|
{% if use_dishka %}from dependencies.container import container
|
||||||
{% endif %}{% if database != 'none' %}from utils.db import init_db
|
{% endif %}{% if database != 'none' %}from utils.db import init_db
|
||||||
{% endif %}{% if serve_spa %}from utils.env import env
|
{% endif %}from utils.env import env
|
||||||
{% endif %}from utils.logging import setup_logging
|
from utils.logging import setup_logging
|
||||||
{% if serve_spa %}
|
{% if serve_spa %}
|
||||||
IMMUTABLE_HEADERS = {"Cache-Control": "public, max-age=31536000, immutable"}
|
IMMUTABLE_HEADERS = {"Cache-Control": "public, max-age=31536000, immutable"}
|
||||||
NO_CACHE_HEADERS = {"Cache-Control": "no-cache"}
|
NO_CACHE_HEADERS = {"Cache-Control": "no-cache"}
|
||||||
@@ -29,7 +29,11 @@ async def lifespan(app_: FastAPI) -> AsyncGenerator[None]:
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="{{ project_name }} API", lifespan=lifespan)
|
app = FastAPI(
|
||||||
|
title="{{ project_name }} API",
|
||||||
|
lifespan=lifespan,
|
||||||
|
openapi_url="/openapi.json" if env.api.docs else None,
|
||||||
|
)
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
from .modules.client import DEVICE_MODEL, PyroClient
|
||||||
|
|
||||||
|
__all__ = ["DEVICE_MODEL", "PyroClient"]
|
||||||
+14
-4
@@ -1,20 +1,30 @@
|
|||||||
from pyrogram import Client, enums
|
from pyrogram import Client, enums
|
||||||
|
|
||||||
|
DEVICE_MODEL = "{{ project_name[:32] }}"
|
||||||
|
|
||||||
|
|
||||||
class PyroClient(Client):
|
class PyroClient(Client):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name: str, *, workdir: str = "sessions", load_handlers: bool = True
|
self,
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
workdir: str = "sessions",
|
||||||
|
device_model: str | None = None,
|
||||||
|
load_handlers: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name,
|
name,
|
||||||
workdir=workdir,
|
workdir=workdir,
|
||||||
api_id=2040,
|
api_id=2040,
|
||||||
api_hash="b18441a1ff607e10a989891a5462e627",
|
api_hash="b18441a1ff607e10a989891a5462e627",
|
||||||
device_model="Desktop",
|
device_model=device_model or DEVICE_MODEL,
|
||||||
system_version="Windows 11 x64",
|
system_version="Windows 11 x64",
|
||||||
app_version="6.2.4 x64",
|
app_version="7.0.8 x64",
|
||||||
lang_pack="tdesktop",
|
lang_pack="tdesktop",
|
||||||
|
lang_code="en",
|
||||||
|
system_lang_code="en-US",
|
||||||
client_platform=enums.ClientPlatform.DESKTOP,
|
client_platform=enums.ClientPlatform.DESKTOP,
|
||||||
|
max_concurrent_transmissions=4,
|
||||||
)
|
)
|
||||||
|
|
||||||
if load_handlers:
|
if load_handlers:
|
||||||
@@ -24,4 +34,4 @@ class PyroClient(Client):
|
|||||||
self.add_handler(*handler)
|
self.add_handler(*handler)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["PyroClient"]
|
__all__ = ["DEVICE_MODEL", "PyroClient"]
|
||||||
+11
-5
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import signal
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
import uvloop
|
import uvloop
|
||||||
@@ -11,8 +12,15 @@ import uvloop
|
|||||||
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
|
||||||
|
SCAN_INTERVAL = 10
|
||||||
|
|
||||||
|
|
||||||
async def runner() -> None:
|
async def runner() -> None:
|
||||||
|
stop = asyncio.Event()
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
loop.add_signal_handler(sig, stop.set)
|
||||||
|
|
||||||
{% if database != 'none' %} await init_db()
|
{% if database != 'none' %} await init_db()
|
||||||
|
|
||||||
{% endif %} sessions_dir = anyio.Path("sessions")
|
{% endif %} sessions_dir = anyio.Path("sessions")
|
||||||
@@ -22,7 +30,7 @@ async def runner() -> None:
|
|||||||
clients: dict[int, PyroClient] = {}
|
clients: dict[int, PyroClient] = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while not stop.is_set():
|
||||||
current = {path.name async for path in sessions_dir.glob("*.session")}
|
current = {path.name async for path in sessions_dir.glob("*.session")}
|
||||||
|
|
||||||
for session_file in current - started:
|
for session_file in current - started:
|
||||||
@@ -43,7 +51,8 @@ async def runner() -> None:
|
|||||||
if me:
|
if me:
|
||||||
clients[me.id] = client
|
clients[me.id] = client
|
||||||
|
|
||||||
await asyncio.sleep(10)
|
with contextlib.suppress(TimeoutError):
|
||||||
|
await asyncio.wait_for(stop.wait(), SCAN_INTERVAL)
|
||||||
finally:
|
finally:
|
||||||
for client in clients.values():
|
for client in clients.values():
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
@@ -55,8 +64,5 @@ def main() -> None:
|
|||||||
uvloop.install()
|
uvloop.install()
|
||||||
|
|
||||||
logger.info("Starting...")
|
logger.info("Starting...")
|
||||||
|
|
||||||
with contextlib.suppress(KeyboardInterrupt):
|
|
||||||
asyncio.run(runner())
|
asyncio.run(runner())
|
||||||
|
|
||||||
logger.info("[red]Stopped.[/]")
|
logger.info("[red]Stopped.[/]")
|
||||||
+7
-4
@@ -1,5 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import signal
|
||||||
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
from apscheduler.triggers.interval import IntervalTrigger
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
@@ -15,6 +15,10 @@ async def example_job() -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def runner() -> None:
|
async def runner() -> None:
|
||||||
|
stop = asyncio.Event()
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
loop.add_signal_handler(sig, stop.set)
|
||||||
{%- if database != 'none' %}
|
{%- if database != 'none' %}
|
||||||
await init_db()
|
await init_db()
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
@@ -27,14 +31,13 @@ async def runner() -> None:
|
|||||||
scheduler.start()
|
scheduler.start()
|
||||||
logger.info("Scheduler started")
|
logger.info("Scheduler started")
|
||||||
|
|
||||||
with contextlib.suppress(asyncio.CancelledError):
|
await stop.wait()
|
||||||
await asyncio.Event().wait()
|
scheduler.shutdown()
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
|
||||||
logger.info("Starting...")
|
logger.info("Starting...")
|
||||||
with contextlib.suppress(KeyboardInterrupt):
|
|
||||||
asyncio.run(runner())
|
asyncio.run(runner())
|
||||||
logger.info("[red]Stopped.[/]")
|
logger.info("[red]Stopped.[/]")
|
||||||
+16
@@ -39,6 +39,22 @@ broker = ResilientListQueueBroker(env.redis.url).with_result_backend(
|
|||||||
{% endif %}{% if use_dishka %}
|
{% endif %}{% if use_dishka %}
|
||||||
setup_dishka(container, broker)
|
setup_dishka(container, broker)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{%- elif taskiq_broker == 'postgres' -%}
|
||||||
|
{% if use_dishka %}from dishka.integrations.taskiq import setup_dishka
|
||||||
|
{% endif %}{% if 'scheduler' in backend_services %}from taskiq import TaskiqScheduler
|
||||||
|
from taskiq.schedule_sources import LabelScheduleSource
|
||||||
|
{% endif %}from taskiq_pg import AsyncpgBroker, AsyncpgResultBackend
|
||||||
|
|
||||||
|
{% if use_dishka %}from dependencies.container import container
|
||||||
|
{% endif %}from utils.env import env
|
||||||
|
|
||||||
|
broker = AsyncpgBroker(env.db.connection_url).with_result_backend(
|
||||||
|
AsyncpgResultBackend(env.db.connection_url)
|
||||||
|
)
|
||||||
|
{% if 'scheduler' in backend_services %}scheduler = TaskiqScheduler(broker, sources=[LabelScheduleSource(broker)])
|
||||||
|
{% endif %}{% if use_dishka %}
|
||||||
|
setup_dishka(container, broker)
|
||||||
|
{% endif %}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{% if use_dishka %}from dishka.integrations.taskiq import setup_dishka
|
{% if use_dishka %}from dishka.integrations.taskiq import setup_dishka
|
||||||
{% endif %}from taskiq import InMemoryBroker{% if 'scheduler' in backend_services %}, TaskiqScheduler
|
{% endif %}from taskiq import InMemoryBroker{% if 'scheduler' in backend_services %}, TaskiqScheduler
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{%- set targets = [] -%}
|
||||||
|
{%- if database == 'postgres' %}{% set _ = targets.append("`make migrate` (`make migrate downgrade -1` to roll back), `make revision m=\"message\"`") %}{% endif -%}
|
||||||
|
{%- if dynamic_config and backend_services %}{% set _ = targets.append("`make script name`") %}{% endif -%}
|
||||||
|
{%- if 'kurigram_userbot' in backend_services %}{% set _ = targets.append("`make session`") %}{% endif -%}
|
||||||
|
# {{ project_name }}
|
||||||
|
{% if project_description and project_description != project_name %}
|
||||||
|
{{ project_description }}
|
||||||
|
{% endif %}
|
||||||
|
Runs in Docker, locally the same way as in production. `COMPOSE_PROFILES` in `.env` selects the services that run on this machine. `docker-compose.override.yml` holds what differs between machines: published ports and the aliases on the external `caddy` network.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make env
|
||||||
|
make recreate
|
||||||
|
```
|
||||||
|
|
||||||
|
`make env` creates `.env` and `docker-compose.override.yml` from their examples. `make recreate` starts the selected profiles, `make rebuild` builds the image first, `make logs`, `make restart` and `make down` do what they say.
|
||||||
|
|
||||||
|
{% if backend_services %}To deploy, `git pull && make deploy`. It runs `make pre-deploy`, which builds the image only when `pyproject.toml`, `uv.lock` or `Dockerfile` changed{% if database == 'postgres' %} and applies migrations{% endif %}, then `docker compose up -d`, then `make post-deploy`, which restarts the services whose mounted code changed. A deploy system with its own `up` step calls the two hooks around it. What was built and started is remembered as `refs/deploy/*` in the clone.
|
||||||
|
|
||||||
|
{% endif %}`make fmt` formats, `make check` runs ruff and ty.{% if targets %} Also {{ targets | join(", ") }}.{% endif %}
|
||||||
Reference in New Issue
Block a user