commit
b787541de8
128
Makefile
Normal file
128
Makefile
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
.PHONY: help build migrate migrate-create start dev stop logs clean test lint format check-db reset-db shell
|
||||
|
||||
# Default target
|
||||
help: ## Show this help message
|
||||
@echo "Available commands:"
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
# Docker build commands
|
||||
build: ## Build Docker containers
|
||||
docker compose build
|
||||
|
||||
# Database commands
|
||||
migrate: build ## Run database migrations in Docker
|
||||
docker compose run --rm backend python -m app.db.init
|
||||
|
||||
migrate-create: ## Create a new migration (use NAME=migration_name)
|
||||
@if [ -z "$(NAME)" ]; then \
|
||||
echo "Usage: make migrate-create NAME=migration_name"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker compose run --rm backend alembic revision --autogenerate -m "$(NAME)"
|
||||
|
||||
migrate-upgrade: ## Upgrade to specific revision (use REV=revision_id)
|
||||
@if [ -z "$(REV)" ]; then \
|
||||
echo "Usage: make migrate-upgrade REV=revision_id"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker compose run --rm backend alembic upgrade "$(REV)"
|
||||
|
||||
migrate-downgrade: ## Downgrade to specific revision (use REV=revision_id)
|
||||
@if [ -z "$(REV)" ]; then \
|
||||
echo "Usage: make migrate-downgrade REV=revision_id"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker compose run --rm backend alembic downgrade "$(REV)"
|
||||
|
||||
migrate-history: ## Show migration history
|
||||
docker compose run --rm backend alembic history
|
||||
|
||||
migrate-current: ## Show current migration revision
|
||||
docker compose run --rm backend alembic current
|
||||
|
||||
check-db: ## Check database connection and status
|
||||
docker compose run --rm backend python -c "from app.db.init import check_database_connection; print('✅ Database OK' if check_database_connection() else '❌ Database connection failed')"
|
||||
|
||||
reset-db: ## Reset database (remove and recreate)
|
||||
@echo "⚠️ This will remove all data! Are you sure? (y/N): "; \
|
||||
read confirm; \
|
||||
if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
|
||||
docker compose down -v; \
|
||||
echo "🗑️ Database reset. Run 'make migrate' to recreate."; \
|
||||
else \
|
||||
echo "❌ Database reset cancelled."; \
|
||||
fi
|
||||
|
||||
# Development commands
|
||||
start: migrate ## Run migrations and start all services
|
||||
docker compose up -d
|
||||
|
||||
dev: migrate ## Run migrations and start services with logs
|
||||
docker compose up
|
||||
|
||||
start-no-migrate: build ## Start services without running migrations
|
||||
docker compose up -d
|
||||
|
||||
dev-no-migrate: build ## Start services with logs without migrations
|
||||
docker compose up
|
||||
|
||||
stop: ## Stop all Docker services
|
||||
docker compose down
|
||||
|
||||
restart: stop start ## Restart all services with migrations
|
||||
|
||||
# Utility commands
|
||||
logs: ## Show Docker container logs
|
||||
docker compose logs -f
|
||||
|
||||
logs-backend: ## Show backend container logs only
|
||||
docker compose logs -f backend
|
||||
|
||||
logs-frontend: ## Show frontend container logs only
|
||||
docker compose logs -f frontend
|
||||
|
||||
shell: ## Open shell in backend Docker container
|
||||
docker compose exec backend /bin/bash
|
||||
|
||||
shell-run: ## Run a new backend container with shell access
|
||||
docker compose run --rm backend /bin/bash
|
||||
|
||||
# Testing and quality (run in Docker)
|
||||
test: ## Run tests in Docker
|
||||
docker compose run --rm backend python -m pytest
|
||||
|
||||
test-verbose: ## Run tests with verbose output in Docker
|
||||
docker compose run --rm backend python -m pytest -v
|
||||
|
||||
lint: ## Run linting in Docker
|
||||
docker compose run --rm backend python -m ruff check .
|
||||
|
||||
format: ## Format code in Docker
|
||||
docker compose run --rm backend python -m ruff format .
|
||||
|
||||
# Cleanup commands
|
||||
clean: ## Clean up Docker containers and images
|
||||
docker compose down --rmi local --volumes --remove-orphans
|
||||
|
||||
clean-all: ## Clean up all Docker resources (containers, images, volumes)
|
||||
docker compose down --rmi all --volumes --remove-orphans
|
||||
docker system prune -f
|
||||
|
||||
# Development workflow commands
|
||||
setup: build migrate ## Initial setup: build containers and run migrations
|
||||
@echo "✅ Setup complete! Run 'make dev' to start the development server."
|
||||
|
||||
deploy-check: build lint test migrate ## Check if ready for deployment
|
||||
@echo "✅ Deployment checks passed!"
|
||||
|
||||
status: ## Show status of Docker services
|
||||
docker compose ps
|
||||
|
||||
# Production commands
|
||||
prod-start: migrate ## Start services in production mode (detached)
|
||||
docker compose up -d
|
||||
|
||||
prod-stop: ## Stop production services
|
||||
docker compose down
|
||||
|
||||
prod-restart: prod-stop prod-start ## Restart production services
|
||||
34
README.md
34
README.md
|
|
@ -36,40 +36,6 @@
|
|||
|
||||
Docker Compose автоматически переопределяет `ALABUGA_SQLITE_PATH=/data/app.db`, чтобы база сохранялась во внешнем volume. Для локального запуска вне Docker оставьте путь `./data/app.db` из примера.
|
||||
|
||||
## Локальная разработка backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
export PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
# подготовьте переменные окружения (однократно)
|
||||
cp .env.example .env
|
||||
|
||||
# при необходимости включите в .env подтверждение почты
|
||||
# ALABUGA_REQUIRE_EMAIL_CONFIRMATION=true
|
||||
|
||||
# база поднимется сама: при старте приложения автоматически выполняется Alembic upgrade head.
|
||||
# Для ручной подготовки можно вызвать команду ниже — она прогоняет миграции и добавляет демо-данные.
|
||||
cd ..
|
||||
python -m scripts.seed_data
|
||||
cd backend
|
||||
|
||||
# Запуск API
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
## Локальная разработка фронтенда
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
cp .env.example .env
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Пользовательские учётные записи (сидированные)
|
||||
|
||||
| Роль | Email | Пароль |
|
||||
|
|
|
|||
|
|
@ -2,17 +2,19 @@ FROM python:3.13-slim
|
|||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
POETRY_VIRTUALENVS_CREATE=false \
|
||||
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends build-essential libpq-dev \
|
||||
&& apt-get install -y --no-install-recommends build-essential libpq-dev curl \
|
||||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
# Install uv
|
||||
RUN pip install --no-cache-dir uv
|
||||
|
||||
COPY pyproject.toml ./
|
||||
RUN uv pip install --system --no-cache -e .
|
||||
|
||||
COPY . /app
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
"""Add email confirmation fields to users"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '20240927_0003'
|
||||
down_revision = '20240611_0002'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column('users', sa.Column('is_email_confirmed', sa.Boolean(), nullable=False, server_default=sa.true()))
|
||||
op.add_column('users', sa.Column('email_confirmation_token', sa.String(length=128), nullable=True))
|
||||
op.add_column('users', sa.Column('email_confirmed_at', sa.DateTime(timezone=True), nullable=True))
|
||||
op.execute('UPDATE users SET is_email_confirmed = 1')
|
||||
op.alter_column('users', 'is_email_confirmed', server_default=None)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('users', 'email_confirmed_at')
|
||||
op.drop_column('users', 'email_confirmation_token')
|
||||
op.drop_column('users', 'is_email_confirmed')
|
||||
|
|
@ -7,7 +7,7 @@ import sqlalchemy as sa
|
|||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '20240927_0004'
|
||||
down_revision = '20240927_0003'
|
||||
down_revision = '20240611_0002'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ def register(user_in: UserRegister, db: Session = Depends(get_db)) -> Token | di
|
|||
token = issue_confirmation_token(user, db)
|
||||
return {
|
||||
"detail": "Мы отправили письмо с подтверждением. Введите код, чтобы активировать аккаунт.",
|
||||
"debug_token": token if settings.environment != 'production' else None,
|
||||
"debug_token": token if settings.debug else None,
|
||||
}
|
||||
|
||||
# 4. Если подтверждение выключено, сразу создаём JWT и возвращаем его фронтенду.
|
||||
|
|
@ -110,7 +110,7 @@ def request_confirmation(payload: EmailRequest, db: Session = Depends(get_db)) -
|
|||
token = issue_confirmation_token(user, db)
|
||||
|
||||
hint = None
|
||||
if settings.environment != 'production':
|
||||
if settings.debug:
|
||||
hint = token
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class Settings(BaseSettings):
|
|||
model_config = SettingsConfigDict(env_file=BASE_DIR / ".env", env_prefix="ALABUGA_", extra="ignore")
|
||||
|
||||
project_name: str = "Alabuga Gamification API"
|
||||
environment: str = "local"
|
||||
debug: bool = False
|
||||
secret_key: str = "super-secret-key-change-me"
|
||||
jwt_algorithm: str = "HS256"
|
||||
access_token_expire_minutes: int = 60 * 12
|
||||
|
|
|
|||
71
backend/app/db/init.py
Normal file
71
backend/app/db/init.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
"""Database initialization and migration utilities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.config import settings
|
||||
from app.db.session import engine
|
||||
|
||||
ALEMBIC_CONFIG = Path(__file__).resolve().parents[2] / "alembic.ini"
|
||||
|
||||
|
||||
def get_alembic_config() -> Config:
|
||||
"""Get configured Alembic Config object."""
|
||||
config = Config(str(ALEMBIC_CONFIG))
|
||||
config.set_main_option("sqlalchemy.url", str(settings.database_url))
|
||||
return config
|
||||
|
||||
|
||||
def check_database_connection() -> bool:
|
||||
"""Check if database connection is working."""
|
||||
try:
|
||||
with engine.connect() as conn:
|
||||
conn.execute(text("SELECT 1"))
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Database connection failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def run_migrations() -> bool:
|
||||
"""Run database migrations to head."""
|
||||
try:
|
||||
config = get_alembic_config()
|
||||
command.upgrade(config, "head")
|
||||
print("✅ Database migrations completed successfully")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Migration failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def init_database() -> bool:
|
||||
"""Initialize database: check connection and run migrations."""
|
||||
print("🔄 Initializing database...")
|
||||
|
||||
if not check_database_connection():
|
||||
print("❌ Cannot connect to database")
|
||||
return False
|
||||
|
||||
if not run_migrations():
|
||||
print("❌ Failed to run migrations")
|
||||
return False
|
||||
|
||||
print("✅ Database initialization completed successfully")
|
||||
return True
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""CLI entry point for database initialization."""
|
||||
success = init_database()
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -2,20 +2,74 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from alembic.script import ScriptDirectory
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.routes import admin, auth, journal, missions, onboarding, store, users
|
||||
from app.core.config import settings
|
||||
from app.db.session import engine
|
||||
from app.core.security import get_password_hash
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.user import User, UserRole
|
||||
from app.models.rank import Rank
|
||||
# Import all models to ensure they're registered with Base.metadata
|
||||
from app import models # This imports all models through the __init__.py
|
||||
|
||||
app = FastAPI(title=settings.project_name)
|
||||
|
||||
|
||||
def create_demo_users() -> None:
|
||||
"""Create demo users if they don't exist."""
|
||||
session: Session = SessionLocal()
|
||||
try:
|
||||
# Check if demo users already exist
|
||||
pilot_exists = session.query(User).filter(User.email == "candidate@alabuga.space").first()
|
||||
hr_exists = session.query(User).filter(User.email == "hr@alabuga.space").first()
|
||||
|
||||
if pilot_exists and hr_exists:
|
||||
print("✅ Demo users already exist")
|
||||
return
|
||||
|
||||
# Get base rank (or None if no ranks exist)
|
||||
base_rank = session.query(Rank).order_by(Rank.required_xp).first()
|
||||
|
||||
# Create pilot demo user
|
||||
if not pilot_exists:
|
||||
pilot = User(
|
||||
email="candidate@alabuga.space",
|
||||
full_name="Алексей Пилотов",
|
||||
role=UserRole.PILOT,
|
||||
hashed_password=get_password_hash("orbita123"),
|
||||
current_rank_id=base_rank.id if base_rank else None,
|
||||
is_email_confirmed=True,
|
||||
preferred_branch="Получение оффера",
|
||||
motivation="Хочу пройти все миссии и закрепиться в экипаже.",
|
||||
)
|
||||
session.add(pilot)
|
||||
print("✅ Created demo pilot user: candidate@alabuga.space / orbita123")
|
||||
|
||||
# Create HR demo user
|
||||
if not hr_exists:
|
||||
hr_rank = session.query(Rank).order_by(Rank.required_xp.desc()).first()
|
||||
hr = User(
|
||||
email="hr@alabuga.space",
|
||||
full_name="Мария HR",
|
||||
role=UserRole.HR,
|
||||
hashed_password=get_password_hash("orbita123"),
|
||||
current_rank_id=hr_rank.id if hr_rank else None,
|
||||
is_email_confirmed=True,
|
||||
preferred_branch="Куратор миссий",
|
||||
)
|
||||
session.add(hr)
|
||||
print("✅ Created demo HR user: hr@alabuga.space / orbita123")
|
||||
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to create demo users: {e}")
|
||||
session.rollback()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
ALEMBIC_CONFIG = Path(__file__).resolve().parents[1] / "alembic.ini"
|
||||
|
||||
app = FastAPI(title=settings.project_name)
|
||||
|
||||
|
|
@ -29,51 +83,6 @@ app.add_middleware(
|
|||
)
|
||||
|
||||
|
||||
def run_migrations() -> None:
|
||||
"""Гарантируем, что база обновлена до последней схемы Alembic."""
|
||||
|
||||
config = Config(str(ALEMBIC_CONFIG))
|
||||
config.set_main_option("sqlalchemy.url", str(settings.database_url))
|
||||
inspector = inspect(engine)
|
||||
script = ScriptDirectory.from_config(config)
|
||||
head_revision = script.get_current_head()
|
||||
|
||||
tables = inspector.get_table_names()
|
||||
current_revision: str | None = None
|
||||
|
||||
if "alembic_version" in tables:
|
||||
with engine.begin() as conn:
|
||||
row = conn.execute(text("SELECT version_num FROM alembic_version LIMIT 1")).fetchone()
|
||||
current_revision = row[0] if row else None
|
||||
|
||||
if "alembic_version" not in tables or current_revision is None:
|
||||
if tables:
|
||||
# База создана через Base.metadata.create_all. Добавляем отсутствующие поля вручную
|
||||
# и фиксируем версию как актуальную, чтобы последующие миграции применялись штатно.
|
||||
user_columns = {col["name"] for col in inspector.get_columns("users")}
|
||||
with engine.begin() as conn:
|
||||
if "preferred_branch" not in user_columns:
|
||||
conn.execute(text("ALTER TABLE users ADD COLUMN preferred_branch VARCHAR(160)"))
|
||||
if "motivation" not in user_columns:
|
||||
conn.execute(text("ALTER TABLE users ADD COLUMN motivation TEXT"))
|
||||
conn.execute(text("CREATE TABLE IF NOT EXISTS alembic_version (version_num VARCHAR(32) NOT NULL)"))
|
||||
conn.execute(text("DELETE FROM alembic_version"))
|
||||
conn.execute(text("INSERT INTO alembic_version (version_num) VALUES (:rev)"), {"rev": head_revision})
|
||||
return
|
||||
# Таблиц ещё нет — создадим их миграциями.
|
||||
command.upgrade(config, "head")
|
||||
return
|
||||
|
||||
command.upgrade(config, "head")
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup() -> None:
|
||||
"""Прогоняем миграции перед обработкой запросов."""
|
||||
|
||||
run_migrations()
|
||||
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(users.router)
|
||||
app.include_router(missions.router)
|
||||
|
|
@ -83,8 +92,15 @@ app.include_router(store.router)
|
|||
app.include_router(admin.router)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Create demo users on startup if in debug mode."""
|
||||
if settings.debug:
|
||||
create_demo_users()
|
||||
|
||||
|
||||
@app.get("/", summary="Проверка работоспособности")
|
||||
def healthcheck() -> dict[str, str]:
|
||||
"""Простой ответ для Docker healthcheck."""
|
||||
|
||||
return {"status": "ok", "environment": settings.environment}
|
||||
return {"status": "ok"}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,33 @@ name = "alabuga-backend"
|
|||
version = "0.1.0"
|
||||
description = "Геймифицированный модуль платформы 'Алабуга'"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"fastapi==0.111.0",
|
||||
"uvicorn[standard]==0.30.1",
|
||||
"SQLAlchemy>=2.0.36,<3",
|
||||
"alembic>=1.14.0,<2",
|
||||
"pydantic==2.9.2",
|
||||
"pydantic-settings==2.10.1",
|
||||
"passlib[bcrypt]==1.7.4",
|
||||
"python-jose[cryptography]==3.3.0",
|
||||
"python-multipart==0.0.9",
|
||||
"bcrypt==4.1.3",
|
||||
"email-validator==2.1.1",
|
||||
"fastapi-pagination==0.12.24",
|
||||
"Jinja2==3.1.4"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest==8.2.2",
|
||||
"pytest-asyncio==0.23.7",
|
||||
"httpx==0.27.0",
|
||||
"coverage==7.5.3",
|
||||
"black==24.4.2",
|
||||
"isort==5.13.2",
|
||||
"ruff==0.4.7",
|
||||
"mypy==1.10.0"
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
-r requirements.txt
|
||||
pytest==8.2.2
|
||||
pytest-asyncio==0.23.7
|
||||
httpx==0.27.0
|
||||
coverage==7.5.3
|
||||
black==24.4.2
|
||||
isort==5.13.2
|
||||
ruff==0.4.7
|
||||
mypy==1.10.0
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
fastapi==0.111.0
|
||||
uvicorn[standard]==0.30.1
|
||||
SQLAlchemy>=2.0.36,<3
|
||||
alembic>=1.14.0,<2
|
||||
pydantic==2.9.2
|
||||
pydantic-settings==2.10.1
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-jose[cryptography]==3.3.0
|
||||
python-multipart==0.0.9
|
||||
bcrypt==4.1.3
|
||||
email-validator==2.1.1
|
||||
fastapi-pagination==0.12.24
|
||||
Jinja2==3.1.4
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
version: '3.9'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
|
|
@ -11,10 +9,10 @@ services:
|
|||
- backend-data:/data
|
||||
- ./backend:/app
|
||||
env_file:
|
||||
- backend/.env.example
|
||||
environment:
|
||||
ALABUGA_ENVIRONMENT: docker
|
||||
- backend/.env
|
||||
depends_on: []
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
frontend:
|
||||
build:
|
||||
|
|
@ -22,18 +20,21 @@ services:
|
|||
command: npm run dev -- --hostname 0.0.0.0 --port 3000
|
||||
ports:
|
||||
- '3000:3000'
|
||||
env_file:
|
||||
- frontend/.env
|
||||
environment:
|
||||
NEXT_PUBLIC_API_URL: http://localhost:8000
|
||||
NEXT_INTERNAL_API_URL: http://backend:8000
|
||||
NEXT_PUBLIC_DEMO_EMAIL: candidate@alabuga.space
|
||||
NEXT_PUBLIC_DEMO_PASSWORD: orbita123
|
||||
NEXT_PUBLIC_DEMO_HR_EMAIL: hr@alabuga.space
|
||||
NEXT_PUBLIC_DEMO_HR_PASSWORD: orbita123
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
volumes:
|
||||
backend-data:
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user