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.
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
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 pdimport plotly.express as pxscores = 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 logingcloud auth application-default logingcloud config set project YOUR_PROJECT_ID
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:
Your laptop: interactive gcloud auth login stores local credentials outside the repository.
Cloud runtime: use the Cloud Run service identity and managed environment/secrets.
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:
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:
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
---title: "Model Deployment III — Where Should Your ML App Live?"subtitle: "Hosting Alternatives, Cost–Complexity Trade-offs, and a Practical Deployment Decision"author: "MLStation"format: html: toc: true toc-depth: 3 code-fold: true code-summary: "Show code" code-tools: true code-copy: true smooth-scroll: true theme: cosmoexecute: echo: true warning: false freeze: auto---<style>.hero{background:linear-gradient(135deg,#7c3aed,#db2777);color:white;padding:2rem;border-radius:18px;margin:1rem01.5rem;box-shadow:08px24px#0002}.hero h2{color:white;margin-top:0}.concept{border-left:6pxsolid#7c3aed;background:#f5f3ff;padding:1rem1.2rem;border-radius:10px;margin:1rem0}.action{border-left:6pxsolid#db2777;background:#fff1f7;padding:1rem1.2rem;border-radius:10px;margin:1rem0}.warningbox{border-left:6pxsolid#f59e0b;background:#fff8e7;padding:1rem1.2rem;border-radius:10px;margin:1rem0}.result{border-left:6pxsolid#16a34a;background:#effcf3;padding:1rem1.2rem;border-radius:10px;margin:1rem0}.navbtn{display:inline-block;padding:.65rem1rem;margin:.25rem;background:#7c3aed;color:white!important;text-decoration:none;border-radius:9px;font-weight:700}.choice{border:2pxsolid#7c3aed;border-radius:16px;padding:1.2rem;background:#faf7ff;margin:1rem0}.pipeline{background:#0f172a;color:#e2e8f0;padding:1.1rem;border-radius:12px;overflow:auto}</style><div class="hero"><h2>Part III: deployment is a choice, not a single command</h2><p>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.</p></div><a class="navbtn" href="model-deployment-part-1.html">← Part I: Build the ML product</a><a class="navbtn" href="model-deployment-part-2.html">← Part II: Docker & Cloud Run</a>## 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 |<div class="concept"><strong>GitHub alternatives are not automatically deployment alternatives.</strong> 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.</div>## 2. Source-code alternatives to GitHubReasonable 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 applicationPricing 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 mathematicallyA 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.```{python}#| code-fold: true#| code-summary: "Show code — editable platform decision matrix"import pandas as pdimport plotly.express as pxscores = 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()```<div class="warningbox"><strong>This score is not a universal ranking.</strong> The numerical ratings above are an explicit teaching assumption, not an objective benchmark. Change the weights and scores for your workload.</div>## 5. Our choice: Google Cloud Run<div class="choice"><strong>Chosen platform for the full series: Google Cloud Run.</strong><br><br>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.</div>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 recapThis 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```{.bash}gcloud auth logingcloud auth application-default logingcloud config set project YOUR_PROJECT_ID```### Step 2 — Enable services```{.bash}gcloud services enable run.googleapis.com \ cloudbuild.googleapis.com \ artifactregistry.googleapis.com```### Step 3 — Variables```{.bash}exportPROJECT_ID="YOUR_PROJECT_ID"exportREGION="us-central1"exportREPOSITORY="energy-ml"```### Step 4 — Create image repository```{.bash}gcloud artifacts repositories create $REPOSITORY\--repository-format=docker \--location=$REGION```### Step 5 — Build and deploy Flask```{.bash}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:```{.bash}exportAPI_URL="https://YOUR-API-URL.run.app"curl"$API_URL/health"```### Step 6 — Build and deploy Streamlit```{.bash}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```<div class="result"><strong>Result.</strong> 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.</div>## 7. Where credentials belongNever 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:```{.text filename=".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:```textGitHub 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:```textVPS├── 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 tutorialAs 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.```textGit push │ ▼CI tests ──> image build ──> registry ──> staging ──> production │ │ └── metrics ───┘```## 12. Final mental model<div class="pipeline">DATA creates evidence<br>↓<br>TRAINING creates a fitted MODEL ARTIFACT<br>↓<br>FLASK loads the artifact and creates an API<br>↓<br>STREAMLIT creates the human interface<br>↓<br>DOCKER makes each service portable<br>↓<br>GITHUB versions source code<br>↓<br>ARTIFACT REGISTRY stores container images<br>↓<br>CLOUD RUN executes and scales the containers</div><a class="navbtn" href="model-deployment-part-1.html">← Part I: Build it</a><a class="navbtn" href="model-deployment-part-2.html">← Part II: Containerize & deploy it</a>---### 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