Model Deployment III — Where Should Your ML App Live?

Hosting Alternatives, Cost–Complexity Trade-offs, and a Practical Deployment Decision

Author

MLStation

Part III: deployment is a choice, not a single command

Parts I and II built and containerized a real ML product. This final article separates source hosting from application hosting, compares practical low-cost choices, and explains why we choose Google Cloud Run for the complete two-service example.

← Part I: Build the ML product ← Part II: Docker & Cloud Run

1. First resolve a common ambiguity: “Where do I upload my content?”

There are at least three different things you may want to host:

Asset Examples What it needs
Source code .py, .qmd, Dockerfiles Git repository / source host
Static educational site rendered MLStation HTML, images static site host/CDN
Running ML application Flask API, Streamlit, XGBoost model compute service/container runtime
GitHub alternatives are not automatically deployment alternatives. GitLab or Codeberg can store source. They do not by themselves replace a runtime such as Cloud Run. Conversely, Cloud Run executes containers but is not where you should author/version your source code.

2. Source-code alternatives to GitHub

Reasonable Git-based choices include GitLab, Bitbucket, Codeberg, and self-hosted Git services such as Gitea/Forgejo. For MLStation, GitHub remains convenient because Quarto/GitHub Pages and many deployment platforms integrate with it well. There is no technical requirement that the model-serving architecture use GitHub.

3. Low-cost places to run an ML application

Pricing and free-tier rules change. Treat the table as an architectural comparison and verify current pricing before committing a production workload.

Platform Best fit Container support Scale-to-zero / sleeping Beginner experience Main caution
Streamlit Community Cloud Streamlit demos/teaching App-oriented Managed ★★★★★ Primarily the UI/app path; Flask is better hosted separately
Google Cloud Run Containerized APIs + UIs Yes Yes ★★★★☆ Requires cloud project/billing setup and cost awareness
Render Simple web services Yes Free/hobby options may sleep ★★★★★ Free/hobby limitations; verify current plan
Railway Developer-friendly services Yes Plan-dependent ★★★★★ Usage/pricing model changes; watch credits/cost
Koyeb Containers/web services Yes Plan-dependent ★★★★☆ Region/resource availability varies by plan
Hugging Face Spaces ML demos, Gradio/Streamlit via Docker Docker Spaces Hardware/plan-dependent ★★★★★ Compute Space creation/hardware rules differ from older “free Spaces” tutorials
Small VPS Maximum control Yes No automatic scale-to-zero ★★☆☆☆ You administer OS, TLS, firewall, patches and uptime

4. How to choose mathematically

A simple monthly decision model is

C_{total}=C_{compute}+C_{memory}+C_{requests}+C_{network}+C_{storage}+C_{operations}.

The last term is easy to ignore. A $5 server that takes hours each month to patch, secure and recover may be more expensive for you than a managed service with a slightly higher invoice.

For a pedagogical project, score each platform using weighted criteria:

S=\sum_{j=1}^{m} w_j s_j, \qquad \sum_j w_j=1.

For example, MLStation might weight simplicity 0.30, low idle cost 0.25, container portability 0.20, scalability 0.15 and production relevance 0.10.

Show code — editable platform decision matrix
import pandas as pd
import plotly.express as px

scores = pd.DataFrame({
    "platform":["Cloud Run","Streamlit Cloud","Render","Railway","Koyeb","Hugging Face Spaces","VPS"],
    "simplicity":[4,5,5,5,4,5,2],
    "low_idle_cost":[5,5,4,3,4,3,2],
    "container_portability":[5,2,5,5,5,4,5],
    "scalability":[5,3,4,4,4,3,3],
    "production_learning":[5,3,4,4,4,3,5]
})
weights = {"simplicity":.30,"low_idle_cost":.25,"container_portability":.20,
           "scalability":.15,"production_learning":.10}
scores["weighted_score"] = sum(scores[k]*w for k,w in weights.items())
scores = scores.sort_values("weighted_score", ascending=False)
display(scores)
fig = px.bar(scores, x="weighted_score", y="platform", orientation="h", color="weighted_score",
             color_continuous_scale="Viridis", title="Example teaching-project decision score")
fig.update_layout(template="plotly_white", yaxis={"categoryorder":"total ascending"})
fig.show()
platform simplicity low_idle_cost container_portability scalability production_learning weighted_score
0 Cloud Run 4 5 5 5 5 4.70
2 Render 5 4 5 4 4 4.50
3 Railway 5 3 5 4 4 4.25
4 Koyeb 4 4 5 4 4 4.20
1 Streamlit Cloud 5 5 2 3 3 3.90
5 Hugging Face Spaces 5 3 4 3 3 3.80
6 VPS 2 2 5 3 5 3.05
This score is not a universal ranking. The numerical ratings above are an explicit teaching assumption, not an objective benchmark. Change the weights and scores for your workload.

5. Our choice: Google Cloud Run

Chosen platform for the full series: Google Cloud Run.

Why? Our application already has two Docker containers. Cloud Run lets us deploy both without managing servers, gives each an HTTPS URL, supports environment variables and independent scaling, and teaches a container workflow that transfers well to other platforms.

For the simplest classroom demonstration, another valid architecture is Streamlit Community Cloud for the UI plus Cloud Run for Flask. For conceptual consistency, Part II deployed both containers to Cloud Run.

6. Complete Google deployment recap

This section intentionally repeats the essential deployment commands so Part III can stand alone after you have completed Part I’s code and Part II’s Dockerfiles.

Step 1 — Authenticate

gcloud auth login
gcloud auth application-default login
gcloud config set project YOUR_PROJECT_ID

Step 2 — Enable services

gcloud services enable run.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com

Step 3 — Variables

export PROJECT_ID="YOUR_PROJECT_ID"
export REGION="us-central1"
export REPOSITORY="energy-ml"

Step 4 — Create image repository

gcloud artifacts repositories create $REPOSITORY \
  --repository-format=docker \
  --location=$REGION

Step 5 — Build and deploy Flask

gcloud builds submit \
  --tag $REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/energy-api:v1 \
  --file Dockerfile.backend .

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 10

Copy the returned URL:

export API_URL="https://YOUR-API-URL.run.app"
curl "$API_URL/health"

Step 6 — Build and deploy Streamlit

gcloud builds submit \
  --tag $REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/energy-ui:v1 \
  --file Dockerfile.frontend .

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 10
Result. The final Streamlit URL is the address you share with users. Streamlit sends predictions to the Flask URL stored in API_BASE_URL; Flask loads and owns the XGBoost pipeline.

7. Where credentials belong

Never put secrets in Python files or commit them to Git. Distinguish three cases:

  1. Your laptop: interactive gcloud auth login stores local credentials outside the repository.
  2. Cloud runtime: use the Cloud Run service identity and managed environment/secrets.
  3. CI/CD: prefer short-lived workload identity/federation and narrowly scoped service accounts rather than downloadable long-lived keys.

A .gitignore should at minimum protect common local secret files:

.gitignore
.env
.env.*
*.key
*service-account*.json
.venv/
__pycache__/

8. Could the frontend be free while only the API uses Google?

Yes. A useful low-cost teaching architecture is:

GitHub public repo
      │
      ├──> Streamlit Community Cloud ─────┐
      │                                   │ HTTPS
      └──> Cloud Run Flask API <──────────┘
                 │
                 ▼
          XGBoost pipeline

In that design, set the Streamlit application’s API_BASE_URL secret/configuration to the Cloud Run API URL. This is often simpler for a course because Streamlit deployment is nearly frictionless while the backend still demonstrates scalable container serving.

9. What about Hugging Face Spaces?

Hugging Face Spaces remains excellent for ML demos and supports Gradio and Docker-based applications. However, do not copy older tutorials that assume every compute configuration and account state is permanently free. Current account/compute rules can change; check the current Spaces documentation and pricing before making it the default path for students.

10. What about a VPS?

A VPS gives you a Linux machine. That means freedom—but also responsibility:

VPS
├── Linux updates
├── Docker
├── reverse proxy (for example Nginx/Caddy)
├── HTTPS certificates
├── firewall
├── process/container restart policy
├── monitoring
├── backups
└── your Flask + Streamlit containers

It is a valuable Part IV topic, but it adds system-administration concepts that distract from the first deployment lesson.

11. Production architecture beyond this tutorial

As traffic and organizational requirements grow, the next evolution might separate model artifacts from the image, use a model registry, add an authenticated API gateway, place logs/metrics in observability systems, automate builds from GitHub, and maintain staging/production revisions.

Git push
   │
   ▼
CI tests ──> image build ──> registry ──> staging ──> production
                                            │              │
                                            └── metrics ───┘

12. Final mental model

DATA creates evidence
↓
TRAINING creates a fitted MODEL ARTIFACT
↓
FLASK loads the artifact and creates an API
↓
STREAMLIT creates the human interface
↓
DOCKER makes each service portable
↓
GITHUB versions source code
↓
ARTIFACT REGISTRY stores container images
↓
CLOUD RUN executes and scales the containers

← Part I: Build it ← Part II: Containerize & deploy it


Series navigation

Part I: Data → EDA → XGBoost → Flask → Streamlit
Part II: Docker → GitHub → Google Cloud Run
Part III: Hosting alternatives → decision framework → practical deployment choices

Back to top