Model Deployment II — Docker, GitHub, and the Cloud
Containerize Flask + Streamlit and Deploy the ML Product to Google Cloud Run
Part II: make the localhost product portable and public
Part I produced a working XGBoost model, Flask API and Streamlit UI. Now we package each service, reproduce it with Docker, push source to GitHub, and deploy the containers to Google Cloud Run.
← Part I: Data, XGBoost, Flask & Streamlit Part III: Hosting alternatives →
1. Where Part I ended
Part I saved the fitted pipeline at:
artifacts/energy_xgb_pipeline.joblib
and Flask loaded it at startup with:
MODEL = joblib.load(MODEL_PATH)The Streamlit application did not load the model. It called Flask through HTTP. We preserve that architecture in production.
Browser
│
▼
Streamlit container ── HTTPS ──> Flask API container
│
▼
energy_xgb_pipeline.joblib
2. Why Docker?
We will create two images because frontend and backend scale independently.
3. Backend Dockerfile
The backend image includes the serialized model because Flask must load it when the container starts. Create Dockerfile.backend in the project root.
Dockerfile.backend
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY artifacts/ ./artifacts/
COPY backend/ ./backend/
ENV PORT=8080
CMD exec gunicorn --bind :${PORT} --workers 2 --threads 4 --timeout 120 backend.app:appbackend.app:app → importing backend/app.py executes joblib.load(...) once per worker → the worker is ready to answer requests.
Why Gunicorn instead of app.run()?
Flask’s development server is for development. Gunicorn is a production WSGI process manager: it can run multiple workers, handle signals correctly, and work naturally in container platforms.
4. Frontend Dockerfile
Create Dockerfile.frontend.
Dockerfile.frontend
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY frontend/ ./frontend/
ENV PORT=8080
CMD streamlit run frontend/streamlit_app.py \
--server.address=0.0.0.0 \
--server.port=${PORT} \
--server.headless=trueNotice that the frontend image does not copy artifacts/. It needs only the Flask URL.
5. .dockerignore
.dockerignore
.git
.gitignore
__pycache__/
*.pyc
.venv/
venv/
.ipynb_checkpoints/
data/
artifacts/test_predictions.csv
Do not exclude artifacts/energy_xgb_pipeline.joblib; the backend image needs it.
6. Build and run locally
docker build -f Dockerfile.backend -t energy-api:local .
docker build -f Dockerfile.frontend -t energy-ui:local .Create a network and run both containers:
docker network create energy-net
docker run --rm --name energy-api --network energy-net \
-p 8000:8080 energy-api:localIn another terminal:
docker run --rm --name energy-ui --network energy-net \
-p 8501:8080 \
-e API_BASE_URL=http://energy-api:8080 \
energy-ui:localOpen http://localhost:8501.
localhost means “this same container.” Streamlit reaches Flask through Docker DNS name energy-api, not localhost.
7. Docker Compose: one command for the complete product
Create compose.yaml.
compose.yaml
services:
api:
build:
context: .
dockerfile: Dockerfile.backend
ports:
- "8000:8080"
ui:
build:
context: .
dockerfile: Dockerfile.frontend
ports:
- "8501:8080"
environment:
API_BASE_URL: http://api:8080
depends_on:
- apiRun:
docker compose up --buildStop:
docker compose down8. GitHub: source control, not the runtime
GitHub repository ──build──> Container image ──store──> Artifact Registry
│
▼
Cloud Run service
GitHub credentials
For command-line pushes, use either GitHub CLI (gh auth login) or SSH. Do not put a GitHub password/token inside source files.
git add .
git commit -m "Part II: Dockerize API and Streamlit"
git push origin main9. Choose a production target: Google Cloud Run
The architecture becomes:
Internet
│
┌─────────▼─────────┐
│ Streamlit Cloud Run│
└─────────┬─────────┘
│ HTTPS + API_BASE_URL
┌─────────▼─────────┐
│ Flask Cloud Run │
│ + XGBoost artifact│
└───────────────────┘
10. Google Cloud prerequisites and credentials
You need a Google Cloud project with billing enabled and the gcloud CLI installed. Authenticate interactively on your own machine:
gcloud auth login
gcloud auth application-default login
gcloud config set project YOUR_PROJECT_IDgcloud auth login. For automated CI/CD, prefer short-lived identity federation / platform-managed service accounts rather than long-lived keys.
Enable the services used below:
gcloud services enable run.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.comSet shell variables:
export PROJECT_ID="YOUR_PROJECT_ID"
export REGION="us-central1"
export REPOSITORY="energy-ml"11. Create an Artifact Registry repository
Artifact Registry stores container images.
gcloud artifacts repositories create $REPOSITORY \
--repository-format=docker \
--location=$REGION \
--description="MLStation energy deployment images"12. Build the backend image in Google Cloud
Cloud Build reads our Dockerfile and pushes the resulting image into Artifact Registry.
gcloud builds submit \
--tag $REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/energy-api:v1 \
--file Dockerfile.backend .Deploy Flask
gcloud run deploy energy-api \
--image $REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/energy-api:v1 \
--region $REGION \
--allow-unauthenticated \
--port 8080 \
--memory 1Gi \
--cpu 1 \
--min 0 \
--max 10Cloud Run prints an HTTPS service URL. Save it:
export API_URL="https://YOUR-ENERGY-API-URL.run.app"
curl "$API_URL/health"
curl "$API_URL/api/v1/meta"energy_xgb_pipeline.joblib that you tested locally. No training occurs on Cloud Run startup.
13. Build and deploy Streamlit
Build the frontend image:
gcloud builds submit \
--tag $REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/energy-ui:v1 \
--file Dockerfile.frontend .Deploy it and inject the Flask URL as an environment variable:
gcloud run deploy energy-ui \
--image $REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/energy-ui:v1 \
--region $REGION \
--allow-unauthenticated \
--port 8080 \
--set-env-vars API_BASE_URL=$API_URL \
--memory 1Gi \
--cpu 1 \
--min 0 \
--max 10Open the HTTPS URL returned for energy-ui.
14. What “scaling” means here
If one container instance can process roughly \mu requests/second and traffic arrives at rate \lambda, utilization is conceptually related to
\rho=\frac{\lambda}{c\mu},
where c is the number of active instances. Real autoscaling is more nuanced, but the intuition is useful: more concurrent demand eventually requires more serving capacity.
Cloud Run can add instances; --max 10 protects against uncontrolled scaling/cost. --min 0 permits scale-to-zero, trading some first-request latency for lower idle cost.
15. Production improvements
For a teaching project, public unauthenticated prediction is convenient. A serious system should additionally consider authentication, request-size limits, schema validation (for example Pydantic), rate limiting, structured logs, monitoring, model registry/versioning, drift monitoring, CI/CD, vulnerability scanning, and private service-to-service authentication.
16. End state
GitHub source → Docker build → Artifact Registry → Flask Cloud Run
GitHub source → Docker build → Artifact Registry → Streamlit Cloud Run
Browser → Streamlit HTTPS → Flask HTTPS → loaded XGBoost pipeline → prediction
← Revisit Part I Part III → Compare hosting alternatives