Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -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
26 changes: 15 additions & 11 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
17 changes: 15 additions & 2 deletions backend/src/infrastructure/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
8 changes: 8 additions & 0 deletions cli/src/cli/features/_builtins/deploy/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -31,8 +31,8 @@ services:

worker:
build:
context: {{ backend_context }}
dockerfile: Dockerfile
context: {{ build_context }}
dockerfile: {{ backend_dockerfile }}
target: dev
env_file:
- {{ env_file }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -23,8 +23,8 @@ services:

api:
build:
context: {{ backend_context }}
dockerfile: Dockerfile
context: {{ build_context }}
dockerfile: {{ backend_dockerfile }}
target: prod
env_file:
- {{ env_file }}
Expand All @@ -48,8 +48,8 @@ services:

worker:
build:
context: {{ backend_context }}
dockerfile: Dockerfile
context: {{ build_context }}
dockerfile: {{ backend_dockerfile }}
target: base
env_file:
- {{ env_file }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -23,8 +23,8 @@ services:

api:
build:
context: {{ backend_context }}
dockerfile: Dockerfile
context: {{ build_context }}
dockerfile: {{ backend_dockerfile }}
target: prod
env_file:
- {{ env_file }}
Expand All @@ -48,8 +48,8 @@ services:

worker:
build:
context: {{ backend_context }}
dockerfile: Dockerfile
context: {{ build_context }}
dockerfile: {{ backend_dockerfile }}
target: base
env_file:
- {{ env_file }}
Expand Down
Loading