diff --git a/analysis_results.md b/analysis_results.md new file mode 100644 index 0000000..2e855e2 --- /dev/null +++ b/analysis_results.md @@ -0,0 +1,36 @@ +# Cloud Run Deployment Troubleshooting Report + +The deployment failure in Google Cloud Run was caused by the application failing to start and listen on the expected port (8080) within the allocated timeout. + +## Root Cause Analysis + +After investigating the `backend/Dockerfile`, I identified that the **source code was not being copied into the container**. The `Dockerfile` only included `COPY requirements.txt .`, which resulted in a container with all dependencies installed but missing the `app` directory and its logic. Consequently, `uvicorn` was unable to find `app.main:app`, causing the container to crash immediately or fail to bind to the port. + +## Changes Applied + +### 1. Update `backend/Dockerfile` +I modified the `Dockerfile` to include the following improvements: +- **Added `COPY . .`**: Ensured that the backend source code is properly included in the image. +- **Fixed Permissions**: Added a `chown` command to ensure the non-privileged `appuser` owns the application directory, preventing permission errors during runtime. +- **Improved Port Handling**: Updated the `CMD` to use the `${PORT}` environment variable provided by Cloud Run, with a fallback to `8080`. + +```dockerfile +# ... after pip install ... + +# Copy source code and fix permissions +COPY . . +RUN chown -R appuser:appuser /app && \ + chmod 755 /app/model_cache + +# ... + +# Command to run the application using the PORT environment variable +CMD ["sh", "-c", "exec uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8080} --proxy-headers"] +``` + +## Next Steps +1. **Commit the changes**: The fixed `Dockerfile` is now ready to be pushed to the repository. +2. **Re-run the GitHub Action**: PR을 `main` 브랜치에 머지(merge)하면 GitHub Actions 워크플로우가 트리거됩니다. + +> [!TIP] +> Once the deployment is successful, you can verify the status through the Cloud Run URL or the readiness check endpoint: `/ready`. diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..28d11e2 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,35 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +.env +.venv/ +pip-log.txt +pip-delete-this-directory.txt +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +.pytest_cache/ +.vscode/ +.idea/ +.git/ +.github/ +README.md +requirements-dev.txt +pytest.ini +pytest_log.txt +pytest_log_utf8.txt +update_metadata.sql +verify_and_clear.py +download_books.py +check_progress.py +data/ +tests/ +scripts/ diff --git a/backend/Dockerfile b/backend/Dockerfile index b6bfbfd..451c4b3 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -19,8 +19,11 @@ RUN pip install --no-cache-dir -r requirements.txt # Create a non-privileged user first so we can use it for directory ownership RUN adduser --disabled-password --gecos "" appuser && \ - mkdir -p /app/model_cache && \ - chown -R appuser:appuser /app && \ + mkdir -p /app/model_cache + +# Copy source code and fix permissions +COPY . . +RUN chown -R appuser:appuser /app && \ chmod 755 /app/model_cache ENV HF_HOME=/app/model_cache @@ -33,8 +36,9 @@ USER appuser # This runs as 'appuser', matching the runtime environment RUN python -c "from langchain_huggingface import HuggingFaceEmbeddings; HuggingFaceEmbeddings(model_name='sentence-transformers/all-MiniLM-L6-v2', model_kwargs={'device': 'cpu'})" -# Expose the port +# Expose the port (Cloud Run defaults to 8080, but can be overridden by $PORT) EXPOSE 8080 -# Command to run the application (0.0.0.0 for Cloud Run) -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080", "--proxy-headers"] +# Command to run the application using the PORT environment variable with exec form +# We use exec to ensure uvicorn becomes the PID 1 process and handles termination signals properly +CMD ["sh", "-c", "exec uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8080} --proxy-headers"] diff --git a/backend/app/core/config.py b/backend/app/core/config.py index ac53ac5..5b528cf 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,4 +1,5 @@ from pathlib import Path +from pydantic import Field, AliasChoices from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): @@ -9,7 +10,10 @@ class Settings(BaseSettings): # Supabase Settings SUPABASE_URL: str = "" - SUPABASE_SERVICE_KEY: str = "" # Use Service Role Key for backend operations + SUPABASE_SERVICE_KEY: str = Field( + "", + validation_alias=AliasChoices("SUPABASE_SERVICE_KEY", "SUPABASE_SERVICE_ROLE_KEY") + ) # Use Service Role Key for backend operations model_config = SettingsConfigDict( env_file=str(Path(__file__).resolve().parents[3] / ".env"), @@ -18,3 +22,9 @@ class Settings(BaseSettings): ) settings = Settings() + +# Fail-fast validation: ensure essential secrets are configured at startup +if not settings.SUPABASE_URL or not settings.SUPABASE_SERVICE_KEY: + raise RuntimeError( + "SUPABASE_URL and SUPABASE_SERVICE_KEY (or SUPABASE_SERVICE_ROLE_KEY) must be configured in environment variables." + )