diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..cfd06a65 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +# The compose files build with the WORKSPACE ROOT as context (uv.lock lives +# here), so keep the context lean and free of secrets. +.git +.venv +**/__pycache__ +**/*.pyc +**/.pytest_cache +**/.mypy_cache +**/.ruff_cache +**/*.egg-info +docs/ +site/ +nginx/ +docker-compose.yml +.env +**/.env diff --git a/backend/Dockerfile b/backend/Dockerfile index d1eb706c..72f72aac 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -7,17 +7,21 @@ WORKDIR /tmp # Use official uv image with pinned version for reproducibility COPY --from=ghcr.io/astral-sh/uv:0.9.9 /uv /usr/local/bin/uv -# Copy both pyproject.toml and uv.lock for deterministic export +# Copy the workspace manifests + lock for deterministic export. The build +# context must be the WORKSPACE ROOT: uv.lock lives there (not in backend/), +# and resolving the workspace needs every member manifest. COPY pyproject.toml uv.lock ./ +COPY backend/pyproject.toml backend/pyproject.toml +COPY cli/pyproject.toml cli/pyproject.toml # Export from lock file (not re-resolving dependencies!) -# Filter out the local project line and keep only external dependencies -RUN uv export --no-dev --no-editable -o /tmp/req-full.txt && \ - grep -v "^\\.$" /tmp/req-full.txt > requirements-prod.txt +# Filter out the local project line ("./backend") and keep only external deps +RUN uv export --package fastapi-boilerplate --no-dev --no-editable -o /tmp/req-full.txt && \ + grep -vE "^\.(/backend)?$" /tmp/req-full.txt > requirements-prod.txt -# Export dev requirements from lock file -RUN uv export --no-editable -o /tmp/req-dev-full.txt && \ - grep -v "^\\.$" /tmp/req-dev-full.txt > requirements-dev.txt +# Export dev requirements (the backend's `dev` extra) from lock file +RUN uv export --package fastapi-boilerplate --extra dev --no-editable -o /tmp/req-dev-full.txt && \ + grep -vE "^\.(/backend)?$" /tmp/req-dev-full.txt > requirements-dev.txt # ===================== Production Base Stage ===================== FROM python:3.11-slim AS base @@ -40,7 +44,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \ rm requirements-prod.txt # Copy source code -COPY src ./src +COPY backend/src ./src # Set Python path ENV PYTHONPATH=/app/src @@ -57,7 +61,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \ rm requirements-dev.txt # Copy test files for development -COPY tests ./tests +COPY backend/tests ./tests # Create non-root user for security (same as production) RUN groupadd -r appuser && useradd -r -m -g appuser appuser @@ -77,8 +81,8 @@ FROM base AS migrate ARG DATABASE_URL="" # Copy migration files -COPY migrations ./migrations -COPY alembic.ini . +COPY backend/migrations ./migrations +COPY backend/alembic.ini . # Create non-root user for security RUN groupadd -r appuser && useradd -r -m -g appuser appuser diff --git a/backend/src/infrastructure/config/settings.py b/backend/src/infrastructure/config/settings.py index e91a6d3c..e9d2ab6e 100644 --- a/backend/src/infrastructure/config/settings.py +++ b/backend/src/infrastructure/config/settings.py @@ -10,18 +10,31 @@ logger = logging.getLogger(__name__) current_file_dir = os.path.dirname(os.path.realpath(__file__)) +backend_root = os.path.abspath(os.path.join(current_file_dir, "..", "..", "..")) project_root = os.path.abspath(os.path.join(current_file_dir, "..", "..", "..", "..")) +# backend/.env is the documented location (README: `cp backend/.env.example +# backend/.env`); the workspace root is kept for backwards compatibility and +# /app/.env is where the container mounts it. env_paths = [ "/app/.env", + os.path.join(backend_root, ".env"), os.path.join(project_root, ".env"), "/.env", ] env_path = next((path for path in env_paths if os.path.isfile(path)), env_paths[0]) -logger.info(f"Using environment file at: {env_path}") -config = Config(env_path) +# Under pytest, skip .env loading: the unit tests assert code defaults, and a +# developer's real backend/.env must not leak into them. The suite declares +# ENVIRONMENT=pytest (pytest-env); PYTEST_VERSION covers runs without that +# plugin (set by pytest itself since 8.1). +_under_pytest = os.environ.get("ENVIRONMENT") == "pytest" or "PYTEST_VERSION" in os.environ +if _under_pytest: + config = Config() +else: + logger.info(f"Using environment file at: {env_path}") + config = Config(env_path) class EnvironmentOption(StrEnum): diff --git a/cli/src/cli/features/_builtins/deploy/feature.py b/cli/src/cli/features/_builtins/deploy/feature.py index f0b07b55..df7e12ba 100644 --- a/cli/src/cli/features/_builtins/deploy/feature.py +++ b/cli/src/cli/features/_builtins/deploy/feature.py @@ -44,6 +44,12 @@ def plan(self, params: dict[str, Any], project: ProjectContext) -> FeaturePlan: redis_image = params.get("redis_image", "redis:7-alpine") nginx_image = params.get("nginx_image", "nginx:1.27-alpine") backend_context = params.get("backend_context", "./backend") + # The image build uses the WORKSPACE ROOT as context - uv.lock and the + # member manifests live there, not in backend/ - so the Dockerfile path + # is context-relative. backend_context stays a host-side path for + # volume mounts and the env file. + build_context = params.get("build_context", ".") + backend_dockerfile = params.get("backend_dockerfile", "backend/Dockerfile") env_file = params.get("env_file", "./backend/.env") context = { @@ -55,6 +61,8 @@ def plan(self, params: dict[str, Any], project: ProjectContext) -> FeaturePlan: "redis_image": redis_image, "nginx_image": nginx_image, "backend_context": backend_context, + "build_context": build_context, + "backend_dockerfile": backend_dockerfile, "env_file": env_file, } diff --git a/cli/src/cli/features/_builtins/deploy/templates/local/docker-compose.yml.j2 b/cli/src/cli/features/_builtins/deploy/templates/local/docker-compose.yml.j2 index 42daba99..4acfbc5f 100644 --- a/cli/src/cli/features/_builtins/deploy/templates/local/docker-compose.yml.j2 +++ b/cli/src/cli/features/_builtins/deploy/templates/local/docker-compose.yml.j2 @@ -7,8 +7,8 @@ name: {{ project_name }} services: api: build: - context: {{ backend_context }} - dockerfile: Dockerfile + context: {{ build_context }} + dockerfile: {{ backend_dockerfile }} target: dev env_file: - {{ env_file }} @@ -31,8 +31,8 @@ services: worker: build: - context: {{ backend_context }} - dockerfile: Dockerfile + context: {{ build_context }} + dockerfile: {{ backend_dockerfile }} target: dev env_file: - {{ env_file }} diff --git a/cli/src/cli/features/_builtins/deploy/templates/nginx/docker-compose.yml.j2 b/cli/src/cli/features/_builtins/deploy/templates/nginx/docker-compose.yml.j2 index 65c22985..85399e5d 100644 --- a/cli/src/cli/features/_builtins/deploy/templates/nginx/docker-compose.yml.j2 +++ b/cli/src/cli/features/_builtins/deploy/templates/nginx/docker-compose.yml.j2 @@ -8,8 +8,8 @@ name: {{ project_name }} services: migrate: build: - context: {{ backend_context }} - dockerfile: Dockerfile + context: {{ build_context }} + dockerfile: {{ backend_dockerfile }} target: migrate env_file: - {{ env_file }} @@ -23,8 +23,8 @@ services: api: build: - context: {{ backend_context }} - dockerfile: Dockerfile + context: {{ build_context }} + dockerfile: {{ backend_dockerfile }} target: prod env_file: - {{ env_file }} @@ -48,8 +48,8 @@ services: worker: build: - context: {{ backend_context }} - dockerfile: Dockerfile + context: {{ build_context }} + dockerfile: {{ backend_dockerfile }} target: base env_file: - {{ env_file }} diff --git a/cli/src/cli/features/_builtins/deploy/templates/prod/docker-compose.yml.j2 b/cli/src/cli/features/_builtins/deploy/templates/prod/docker-compose.yml.j2 index 5f457bb7..911664be 100644 --- a/cli/src/cli/features/_builtins/deploy/templates/prod/docker-compose.yml.j2 +++ b/cli/src/cli/features/_builtins/deploy/templates/prod/docker-compose.yml.j2 @@ -8,8 +8,8 @@ name: {{ project_name }} services: migrate: build: - context: {{ backend_context }} - dockerfile: Dockerfile + context: {{ build_context }} + dockerfile: {{ backend_dockerfile }} target: migrate env_file: - {{ env_file }} @@ -23,8 +23,8 @@ services: api: build: - context: {{ backend_context }} - dockerfile: Dockerfile + context: {{ build_context }} + dockerfile: {{ backend_dockerfile }} target: prod env_file: - {{ env_file }} @@ -48,8 +48,8 @@ services: worker: build: - context: {{ backend_context }} - dockerfile: Dockerfile + context: {{ build_context }} + dockerfile: {{ backend_dockerfile }} target: base env_file: - {{ env_file }}