A minimal "Hello World" Flask application intended for deployment to Google Cloud Run using source deployment (Cloud Buildpacks).
Why this project
- Demonstrates a small, production-ready Python/Flask layout suitable for Cloud Run.
- Uses
gunicornin production (viaProcfile) and the Flask dev server for local work. - Dependency-friendly:
uvmanages dependencies viapyproject.tomlanduv.lockfor local development, withrequirements.txtgenerated for Cloud Run Buildpacks.
Quick overview
- Main app:
app.py(Flask application instanceapp) - Start command (Cloud Run / production):
gunicorn --bind :$PORT --workers 1 --threads 8 app:app(seeProcfile) - Flask version:
3.1.2 - Gunicorn version:
23.0.0
This project uses uv to manage dependencies and the development workflow.
-
Install
uv: Follow the official instructions to installuv. -
Create and Sync the Virtual Environment: This command creates a virtual environment, generates the
uv.lockfile with exact package versions, and installs all production and development dependencies.uv sync --all-extras
-
Activate the virtual environment:
source .venv/bin/activate -
Run the development server:
FLASK_DEBUG=1 uv run flask run
The application will be available at
http://127.0.0.1:5000.
Flask's debug mode is a powerful feature for local development that provides:
- Interactive Debugger: Catches unhandled exceptions and allows you to inspect the code state in your browser.
- Automatic Reloader: Automatically restarts the server when code changes are detected, so you don't have to manually restart it after every modification.
Controlling Debug Mode:
- By default, we've enabled it for local development using
FLASK_DEBUG=1. - To explicitly disable debug mode (e.g., for performance testing or if you prefer manual restarts), you can set
FLASK_DEBUG=0:FLASK_DEBUG=0 uv run flask run
For more details, refer to the Flask Debug Mode documentation.
When you run the server in debug mode for the first time, you will see a "Debugger PIN" in your console output. If your application encounters an error, the interactive debugger will start in your browser. You will be prompted to enter this PIN to unlock the full interactive features. This is a security measure to prevent unauthorized users from executing code on your machine. This is another critical reason why debug mode must never be used in production.
This project uses ruff for linting and pytest for testing. Both are configured as development dependencies.
-
To run the linter:
uv run ruff check . -
To run the tests (without coverage):
uv run python -m pytest
You can run specific tests by passing arguments to pytest:
-
Run all tests in a file:
uv run python -m pytest tests/test_app.py
-
Run a single test function by name:
uv run python -m pytest tests/test_app.py::test_root
This project uses pytest-cov to measure code coverage. The coverage source (the app module) and the minimum coverage threshold (90%) are configured centrally in pyproject.toml.
Unlike the default test run, running a coverage analysis is an explicit action.
- To run tests and enforce coverage:
Use the
--covflag. This will activatepytest-cov, which will then use the settings frompyproject.tomlto measure coverage and fail if the threshold is not met.uv run python -m pytest --cov
This project uses pytest-timeout to prevent tests from running indefinitely, which can be crucial in CI/CD pipelines or large test suites.
-
Global Timeout: A default global timeout is configured in
pyproject.tomlunder[tool.pytest.ini_options](e.g.,timeout = "10"seconds). -
Per-Test/Per-Module Timeout: You can override the global timeout or set specific timeouts using
pytestmarkers:import pytest import time @pytest.mark.timeout(5) # This test will time out after 5 seconds def test_long_running_task(): time.sleep(6) # This will cause a timeout assert True @pytest.mark.timeout(timeout=20, method="thread") # Use a thread-based timeout def test_another_long_task(): time.sleep(15) assert True
-
To run tests with timeout enabled (this is automatic when
pyproject.tomlis configured):uv run python -m pytest
To run the application locally using Gunicorn (mimicking the production environment), first ensure your dependencies are installed via uv sync. The uv run command will then execute Gunicorn from within your project's virtual environment.
uv run gunicorn --bind 0.0.0.0:8080 --workers 1 --threads 8 app:app- The application will be available at
http://127.0.0.1:8080. - Press
Ctrl-Cto perform a graceful shutdown of the server.
-
app:app: This tells Gunicorn how to find your application. The format is<module_name>:<variable_name>. In our case, it means: "in theapp.pyfile, find the Flask object namedapp." -
--workers 1 --threads 8: This configures the concurrency model.- Workers are separate OS processes. Multiple workers allow your app to utilize multiple CPU cores and achieve true parallelism.
- Threads are managed within a worker process. Multiple threads allow a single worker to handle multiple I/O-bound requests concurrently (e.g., requests waiting on a database or API call).
- Our choice of 1 worker and 8 threads is a sensible default for a small, single-core environment, allowing one process to handle up to 8 concurrent connections.
This project includes a GitHub Actions workflow (.github/workflows/ci.yml) that automatically runs linting and tests on every push and pull request to the main branch.
The CI workflow:
- Checks out the code.
- Sets up Python 3.13.
- Installs
uvusing the officialastral-sh/setup-uvaction. - Installs all project dependencies using
uv sync --locked --all-extrasto ensure the lock file is up-to-date. - Runs
rufffor linting. - Runs
pytestand enforces the minimum test coverage threshold defined inpyproject.toml.
This project is deployed to Cloud Run using the source deployment method, where Google Cloud Buildpacks automatically build a container image from your source code.
Cloud Run's build process uses the standard requirements.txt file. To ensure the versions in this file exactly match your development environment (defined by uv.lock), generate it using the following command.
uv pip compile pyproject.toml --output-file=requirements.txtThe Procfile is a critical file that tells Cloud Run what command to run to start your web server. Its content is:
web: gunicorn --bind :$PORT --workers 1 --threads 8 app:app
- The
web:label is a process type. For web services, Cloud Run specifically looks for thewebprocess type to start the server that will receive incoming HTTP traffic. For more details on theProcfileformat and other possible process types, you can refer to Heroku's Procfile documentation, which is the standard that Google Cloud Buildpacks follow.
For convenience, it's best to export your Project ID and Region as environment variables.
# Set your project and region
export PROJECT_ID="YOUR_PROJECT_ID" # Replace with your Google Cloud Project ID
export REGION="us-west1"Then, you can run the deployment command without modification.
# Deploy to Cloud Run
gcloud run deploy cloudrun-example \
--source . \
--project=$PROJECT_ID \
--region=$REGION \
--platform=managedThis project is configured for Continuous Deployment to Google Cloud Run using GitHub Actions. Once set up, any changes pushed to the main branch that pass the CI checks will be automatically deployed.
- Trigger: A push to the
mainbranch triggers the workflow. - CI Checks: The
test-and-lintjob runs, ensuring code quality and correctness. - CD Trigger: If the
test-and-lintjob passes, thedeployjob starts. - Authentication: The
deployjob securely authenticates to Google Cloud using Workload Identity Federation. - Deployment: The application is deployed to Cloud Run using the
google-github-actions/deploy-cloudrunaction.
To enable Continuous Deployment, you need to perform a one-time setup in your Google Cloud project and GitHub repository. This is now managed via an Infrastructure as Code (IaC) approach using the Terraform CDK.
Prerequisites: Before you begin, ensure you have the following tools installed:
- Node.js and npm
- Google Cloud SDK (
gcloud)- Python 3.13+
uv(follow the official installation guide)cdktf-cli(install withnpm install -g cdktf-cli)
Cross-Platform Note: The
validate.shscript uses thesha256sumcommand. On macOS, you may need to installcoreutils(e.g., viabrew install coreutils) to get this command, or you can replace it withshasum -a 256.
-
Authenticate with Google Cloud:
gcloud auth login
-
Configure Environment Variables: The IaC script requires your GCP Project ID and GitHub repository details. Export them as environment variables:
export PROJECT_ID="YOUR_GCP_PROJECT_ID" # Replace with your Google Cloud Project ID export REPO="YOUR_GITHUB_USERNAME/YOUR_REPO_NAME" # Replace with your GitHub repository (e.g., "octocat/Spoon-Knife") # export GCP_RUNTIME_SA="your-run-sa@your-gcp-project-id.iam.gserviceaccount.com" # Optional: The runtime SA for your Cloud Run service.
-
Deploy the Infrastructure: Navigate to the
iacdirectory and run the deployment command. This will synthesize the Python code into a Terraform plan and prompt you for confirmation before creating the resources.cd iac uv run cdktf get uv run cdktf synth uv run cdktf deployEnter
yesto approve the deployment. -
Configure GitHub Repository Secrets: After the
cdktf deploycommand successfully completes, it will display the names and values for the three secrets you need to create in your GitHub repository.- Go to
Settings > Environmentsand clickNew environment. - Name it
productionand clickConfigure environment. - In the environment settings, find the
Environment secretssection and clickAdd secretfor each of the three secrets (GCP_PROJECT_ID,GCP_WORKLOAD_IDENTITY_PROVIDER, andGCP_SERVICE_ACCOUNT). - Copy the values from the
Outputssection of thecdktf deploycommand's terminal output.
- Go to
.
├── .gcloudignore # Specifies files to ignore when deploying to Google Cloud
├── app.py # Main Flask application (app:app)
├── Procfile # Production start command used by Cloud Run (gunicorn)
├── pyproject.toml # Project definition and development dependencies for `uv`
├── requirements.txt # Pinned dependencies for production (used by buildpacks)
├── iac/ # Infrastructure as Code (Terraform CDK Python application)
│ ├── .gitignore
│ ├── cdktf.json
│ ├── main.py
│ ├── pyproject.toml
│ ├── requirements.txt
│ └── validate.sh
├── scripts/
│ └── sync-deps.sh # Syncs dependencies and generates requirements.txt
├── LICENSE # MIT license
└── README.md # This file
- The
Procfilemust be in the project root directory for Cloud Run's buildpacks to find it. Other configuration files like.gcloudignorealso reside in the root. - The
.gcloudignorefile prevents specified files and directories from being uploaded to Google Cloud during deployment, reducing build times and preventing sensitive files from being exposed. It is similar in function to.gitignore. - If you depend on system packages (ffmpeg, imagemagick, etc.) or need full control over the runtime, provide a
Dockerfileand build a custom image instead of relying on buildpacks. - For private dependencies, prefer Artifact Registry or authenticated build steps rather than embedding credentials in source.
- Keep
requirements.txtupdated if you change pinned production dependencies.
Contributions are welcome! This project follows a standard fork-and-pull request workflow. Branch protection is enabled for the main branch.
- Fork the repository to your own GitHub account.
- Clone your fork to your local machine.
- Create a new branch for your feature or bug fix (
git checkout -b my-new-feature). - Set up the environment by running
uv sync --all-extras. - Make your changes.
- If you add or change a dependency, modify
pyproject.tomland then run./scripts/sync-deps.shto update the lock file, virtual environment, and production requirements. (Note: You may need to make the script executable first withchmod +x ./scripts/sync-deps.sh).
- If you add or change a dependency, modify
- Run checks locally to ensure your changes pass before pushing.
# Run the linter uv run ruff check . # Run the test suite with coverage uv run python -m pytest --cov
- Commit and push your changes to your fork.
- Open a pull request from your fork's branch to the
mainbranch of the original repository. - Your pull request will be reviewed after the automated CI checks have passed.
This project is licensed under the MIT License - see the LICENSE file for details.
A: While uv run pytest often works, uv run python -m pytest is a more robust and explicit command that is guaranteed to work correctly across different developer machines and shell configurations.
- The
pytestcommand relies on the shell finding thepytestexecutable script in thePATH. This can sometimes fail due to shell caching,PATHconflicts from other tools, or a corrupted executable script. - The
python -m pytestcommand directly uses the project'spythoninterpreter to find and run thepytestmodule. This bypasses the shell'sPATHsearch for thepytestscript, instead using Python's own internal and more reliable module-finding mechanism.
In short, it's the canonical and safest way to run an installed Python module, which is why it is the standard used in this project.
Further Reading:
A: You can use the timeout_func_only configuration option. By default, pytest-timeout applies the timeout to the entire test item, including setup and teardown phases. Setting timeout_func_only = true is useful when you have a long-running setup fixture (e.g., initializing a database, preparing complex data) that you want to exclude from the test's execution time limit.
To enable this, add the following to your pyproject.toml:
[tool.pytest.ini_options]
timeout = "10"
timeout_func_only = true