DEV Community

Wesley Chun (@wescpy)
Wesley Chun (@wescpy)

Posted on

Guide to modern app-hosting without servers on Google Cloud

An in-depth intro to deploying apps on Cloud Run #serverless... TL;DR:

If you're looking to put code online today, you have many options, with virtual machines (VMs) or Kubernetes as the best choice for always-active apps with constant load. Apps with spiky, viral, unpredictable, and even no traffic tend to do better on serverless platforms, those where you don't need to know about nor manage servers. As far as app-hosting goes, Google Cloud (GCP) has App Engine and Cloud Run. App Engine is introduced elsewhere; this post explores its modern, contemporary sibling, Cloud Run.

Serverless computing with Google

[IMAGE] Serverless computing with Google

Introduction

Welcome to the blog focused on showing Python & Node.js developers how to integrate with Google technologies through its APIs and platforms. Here, you'll find content on GCP (AI/ML, serverless, Workspace (GWS; Docs/Drive, Sheets/Drive), Maps, Gemini, or YouTube, not to mention credentials basics like API keys and OAuth client IDs (primarily to access GWS APIs).

Before diving into this long-ish post, stop if you have apps getting constant traffic with predictable loads, because those app-hosting workloads generally do better with VMs on Kubernetes. Serverless tends to be a better fit for apps with unpredictable traffic such as social media, gaming, mobile backend services, startups/go-to-market solutions, student coursework or capstone projects, university research, enterprise intranet sites, and new apps or prototypes. If your apps are in the latter group, keep reading.

Google Cloud Run

If Google App Engine (GAE) is the "OG" serverless platform, Cloud Run (GCR) is its logical successor, crafted for today's modern app-hosting needs. GAE was the 1st generation of Google serverless platforms. It has since been joined, about a decade later, by 2nd generation services, GCR and Cloud Functions (GCF). GCF is somewhat out-of-scope for this post so I'll cover that another time.

📝 Google Cloud Functions is now Cloud Run functions
The 2nd generation GCF product was rebranded as Cloud Run functions in 2024, so expect to see this change if you start digging around for GVF documentation.

There is another post dedicated to introducing GAE, demonstrating how to deploy a "Hello World!" sample app to that platform, so check it out if interested. In this post, we're going to deploy the same app (but) to GCR, and the cumulative knowledge gives you the ability to deploy apps to both platforms.

⚠️ ALERT: Cost: billing required (but "free?!?")
While many Google products are free to use, GCP products are not. In order to run the sample apps, you must enable billing and a billing account backed by a financial instrument like a credit card (payment method depends on region/currency). If you're new to GCP, review the billing & onboarding guide. That said, deploying and running the sample app(s) in this post should not incur any cost because basic usage falls under the free tiers described below along with other important notes about billing:

  1. Several GCP products (like GCR) have an "Always Free" tier, a free daily or monthly usage quota before incurring charges. See the GCR pricing and quotas pages for more information. Furthermore, deploying to GCP serverless platforms incur minor build and storage costs. (Also see similar content in the GAE docs.)
  2. The Cloud Build system has its own free quota as does Cloud Storage (GCS), used to store build artifacts and built container images. The images themselves are also sent to the Cloud Artifact Registry (CAR) making them accessible to other GCP services. They eat into GCS & CAR (storage) quotas as does transferring images between services & regions. You may be in a region that does not have a free tier however, so monitor your usage to minimize any costs.
  3. Use the cost calculator to get monthly estimates. You may qualify for credits to offset GCP costs: If you are a startup, consider the GCP for Startups program grants. If you are in education, check out the GCP education programs for students, faculty, and researchers.

Getting started

Deploying apps to GAE and GCR are similar, and the only differences lie with the configuration files. Both Python & Node.js apps can be found in this repo. I'll walk through each app, run it locally, then deploy to GCR after some setup, starting with Python.

Python

The Python repo features the main app source (main.py), a Python config file (requirements.txt), and the rest, config files for GCR/GCP:

File Info Purpose
main.py (n/a) Main Python application file
requirements.txt info 3rd-party packages requirements
Dockerfile info Docker instructions on how to build container and start application
Procfile info Instructions on how to start application (without Docker)
.dockerignore info Filter files that should not go into container image
.gcloudignore info Filter files that should not be uploaded to GCP
[TABLE] GCR Python repo files

 

📝 Sample app Python 2 & 3 compatible
The sample app is Python 2 & 3 compatible. While Python 2 has been sunset by the community, many users have dependencies that haven't migrated to Python 3 (yet) or have apps they can't migrate for other reasons. GCR is one of the few remaining ways to deploy 2.x apps. The sample defaults to 3.x but contains a commented out directive to use a 2.x base image.

Application files

The requirements.txt file is to specify 3rd-party libraries. The only package listed is the Flask micro web framework, just like the GAE app'srequirements.txt file:

#gunicorn
flask
Enter fullscreen mode Exit fullscreen mode

However, here and commented out but not available in the GAE app's requirements.txt, is Green Unicorn (gunicorn), an HTTP web server. GAE is pure PaaS (Platform-as-a-Service), but GCR, along with any container-based cloud service, is one step down, in-between PaaS and IaaS (Infrastructure-as-a-Service).

As a lower-level service, GCR doesn't provide a web server like GAE does... you have to bring your own. For the purposes of the demo, the Flask development server suffices, but Gunicorn can be easily selected once your app makes its way towards production.

Now let's look at main.py:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def root():
    return 'Hello World!'

# local-only
if __name__ == '__main__':
    import os
    app.run(debug=True, threaded=True, host='0.0.0.0',
            port=int(os.environ.get('PORT', 8080)))
Enter fullscreen mode Exit fullscreen mode

It's line-by-line identical to the GAE main.py: The Flask library is imported, and the Flask app is instantiated. The only route is /, returning 'Hello World!' for GET requests. The last few lines run the Flask development server in debug mode on port 8080 if the PORT environment variable isn't set. It's only started when running main.py as a script (per the if block). (When starting a real server, this entire block is ignored.)

Running locally with Flask (optional)

It's a good idea to test apps locally before deploying to the cloud, so let's install any required packages. In your regular or virtual environment (virtualenv), execute: pip install -r requirements.txt (or pip3 if you also have Python 2 installed). Fresh installs result in output like this:

$ pip3 install -r requirements.txt
Collecting flask (from -r requirements.txt (line 2))
  Downloading flask-3.1.0-py3-none-any.whl.metadata (2.7 kB)
Collecting Werkzeug>=3.1 (from flask->-r requirements.txt (line 2))
  Downloading werkzeug-3.1.3-py3-none-any.whl.metadata (3.7 kB)
Collecting Jinja2>=3.1.2 (from flask->-r requirements.txt (line 2))
  Downloading jinja2-3.1.5-py3-none-any.whl.metadata (2.6 kB)
. . .
Downloading jinja2-3.1.5-py3-none-any.whl (134 kB)
Downloading werkzeug-3.1.3-py3-none-any.whl (224 kB)
Downloading MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl (14 kB)
Installing collected packages: MarkupSafe, itsdangerous, click, blinker, Werkzeug, Jinja2, flask
Successfully installed Jinja2-3.1.5 MarkupSafe-3.0.2 Werkzeug-3.1.3 blinker-1.9.0 click-8.1.8 flask-3.1.0 itsdangerous-2.2.0
Enter fullscreen mode Exit fullscreen mode

With dependencies installed, start the Flask "devserver" with python main.py (or python3):

$ python3 main.py
 * Serving Flask app 'main'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:8080
 * Running on http://192.168.1.147:8080
Press CTRL+C to quit
 * Restarting with watchdog (fsevents)
 * Debugger is active!
 * Debugger PIN: 872-970-164
Enter fullscreen mode Exit fullscreen mode

With a running server, point a web browser to http://localhost:8080:

[IMAGE] "Hello World!" sample app running locally

 

In your terminal, you'll see log entries for each HTTP request:

127.0.0.1 - - [03/Jan/2025 22:34:45] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [03/Jan/2025 22:34:45] "GET /favicon.ico HTTP/1.1" 404 -
Enter fullscreen mode Exit fullscreen mode

To exit the devserver, issue a ^C (Control-C) on the command-line. In the next section, we'll look at another way to run the app locally.

Remaining configuration file(s)

The other config files specify how the app should be containerized, started, and deployed to the cloud. That's the reason why none of them were used to run the app locally just a moment ago. (There is another way to run it locally, with the help of Docker, and we'll take a look at that shortly.) The .*ignore files for this app filter out content that doesn't have anything to do with an app's functionality:

  • .dockerignore -- keeps out files that shouldn't go into a container image
  • .gcloudignore -- filters out files that do not need to be deployed to GCP

The .*ignore files have much in common; the main difference is that package & build files may be needed to build the container but don't need to be deployed to the cloud because they don't play a part in running the app.

The last pair of config files, Dockerfile and Procfile, are the most important: One is required to deploy this app to GCR, and how your containerized app should be built determines which you choose. Let's talk about your options, starting with the Dockerfile.

Using Docker

This is the Python app's Dockerfile:

FROM python:3-slim
#FROM python:2-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
ENTRYPOINT ["python", "main.py"]
#ENTRYPOINT exec gunicorn -b :$PORT -w 2 main:app
Enter fullscreen mode Exit fullscreen mode

These instructions tell Docker how to build the container image and how to start the app within. Its contents are described in this table:

Directive Explanation
FROM Base image to build your container from, a minimal Python 3 installation
FROM (commented out) Use minimal Python 2 base image
WORKDIR Work directory for files going into your container
COPY Copies 3rd-party package file, requirements.txt into work directory
RUN Executes pip install command to install 3rd-party packages listed in requirements.txt
COPY Copies all files into the work directory (including requirements.txt again)
ENTRYPOINT Starts app via Flask devserver
ENTRYPOINT (commented out) Starts app via Gunicorn server
[TABLE] Python app `Dockerfile` directives

 

Running locally with Docker (optional)

With Docker, you can build a container locally and run it, providing an alternative way to test locally before any cloud deployment. It also better simulates how GCR will run your app as well. Ignore the other configuration file, Procfile, for now. While it usually doesn't conflict with the Dockerfile, there are some situations in which a conflict may arise. To avoid this, temporarily rename, delete, or move Procfile elsewhere. With just the Dockerfile present, run these commands to build the container and run the app:

docker build -qt test .
docker run -p 8000:8080 -t test
Enter fullscreen mode Exit fullscreen mode

The build command is as advertised: it builds your container image, naming it test (-t "tag" option) and outputs the unique hash for your container. Drop the optional -q ("quiet") flag to see all kinds of Docker output when building the image.

The run command runs the named container (via -t) and redirects the Flask devserver output to your terminal. It maps requests from port 8000 of your development machine to 8080 in the container (via -p).

Execute both commands as listed and expect output like below, including the Flask request logs like we saw earlier. Be sure to hit the app at http://localhost:8000 -- that's port 8000, not the Flask devserver's 8080 like when running Flask directly earlier:

$ docker build -qt test .
sha256:67b5062c93552271f0d334e3b7e8ef9a83244438cfdbe7a56b117a5d50da70f8

$ docker run -p 8000:8080 -t test
 * Serving Flask app 'main'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:8080
 * Running on http://172.17.0.2:8080
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 257-974-169
172.17.0.1 - - [02/Jan/2025 08:26:25] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [02/Jan/2025 08:26:26] "GET /favicon.ico HTTP/1.1" 404 -
Enter fullscreen mode Exit fullscreen mode

Use ^C (Control-C) to quit the devserver as before, exiting the container. Now you know two different ways to run your app locally. Now let's move beyond Docker & Dockerfile and look at the Procfile.

NOT using Docker

The other way to run a containerized app on GCR is to not use Docker explicitly. If the Dockerfile is missing, Cloud Build uses Buildpacks, a tool built on open standards that automatically inspects application files and dynamically determines the best way to build & containerize apps. This is clearly the option for those new to Docker, don't know Docker, want to avoid Docker, or don't even think about containers. More specifically, Buildpacks adopted by Google is how images can be built & deployed on GCR without Docker.

While you don't have to know anything about Docker when using Buildpacks, having a .dockerignore file is still useful in terms of filtering out files that make building a container inefficient, and most Buildpacks implementations honor the ignore file. The one caveat for Python apps is that without a Dockerfile's ENTRYPOINT directive, it doesn't know how to start your app, and that's why the Procfile is required when not using Docker.

To opt for Buildpacks instead of Docker, move, rename, or remove the Dockerfile. Look inside the Procfile to find the equivalent of Dockerfile's ENTRYPOINT directive describing how to start your app:

web: python main.py
#web: gunicorn -b :$PORT -w 2 main:app
Enter fullscreen mode Exit fullscreen mode

Like the Dockerfile, there's a 2nd, commented-out alternative ENTRYPOINT for using Gunicorn instead of the Flask devserver.

You can install the Buildpacks pack tool to create a containerized app image, but without Docker, you can't run it locally. So for this app, your only local testing option is the Flask devserver.

Python app summary

  • Python users have a choice for testing apps locally: 1) use Flask devserver, or 2) build a container & run it with Docker.
  • To deploy this app to GCR, choose between using Docker (Dockerfile) and not using Docker (Procfile) (remember to move/remove the other config). Either way, keep the .dockerignore file to minimize container size and image build time.
  • When ready to go live, switch from the Flask devserver to something more production-worthy, such as Gunicorn, uWSGI, nginx, or uvicorn.

Node.js

Node developers, it's your turn. Here's what's in this repo:

File Info Purpose
index.js (n/a) Main Node app file (CommonJS)
index.mjs (n/a) Main Node app file (ESmodule)
package.json info 3rd-party packages requirements
Dockerfile info Docker instructions on how to build container and start application
.dockerignore info Filter files that should not go into container image
.gcloudignore info Filter files that should not be uploaded to GCP
[TABLE] GCR Node repo files

 

Application files

Python has Flask, and similarly, Node has the Express micro web framework, the only 3rd-party package found in Node's 3rd-party packages requirements file, package.json:

{
  "name": "helloworld-nodejs",
  "version": "0.0.1",
  "description": "Node.js Cloud Run sample app",
  "main": "index.mjs",
  "scripts": {
    "start": "node index.mjs"
  },
  "author": "CyberWeb Consulting LLC",
  "license": "Apache-2.0",
  "dependencies": {
    "express": "^4.17.1"
  }
}
Enter fullscreen mode Exit fullscreen mode

The app can be found in index.mjs:

import express from 'express';
const app = express();

app.get('/', (req, rsp) => {
  rsp.send('Hello World!');
});

const PORT = process.env.PORT || 8080;
app.listen(PORT, () =>
  console.log(`Listening on port ${PORT}`)
);
export default app;
Enter fullscreen mode Exit fullscreen mode

There's also a CommonJS version, index.js for those who prefer it. With either version, app instantiation is followed by the GET handler returning "Hello World!". The rest sets up the server on the designated PORT and exports the app. Unlike Flask, the Express server is much more performant and can be used in production, perhaps with the help of tools like cluster and PM2.

Running locally with Express.js (optional)

As mentioned earlier, while running locally is optional, it's a good practice. Install the required packages first:

$ npm i

added 69 packages, and audited 70 packages in 2s

found 0 vulnerabilities
Enter fullscreen mode Exit fullscreen mode

You'll see the expected node_modules folder and package-lock.json file. Now launch it with npm start:

$ npm start

> helloworld-nodejs@0.0.1 start
> node index.mjs

Listening on port 8080
Enter fullscreen mode Exit fullscreen mode

Express doesn't log HTTP requests -- developers have to add middleware to them, so don't expect request output like Flask. What is the same is the "Hello World!" you see in your browser when you hit the app at http://localhost:8080. Exit the server with ^C (Control-C).

Remaining configuration file(s)

The remaining config files are: Dockerfile, .dockerignore, and .gcloudignore, identical in functionality to their Python cousins. There's no Procfile because package.json already contains app startup (npm start) instructions.

Running locally with Docker (optional)

You can also build a container and run the app locally with Docker and the Node Dockerfile:

FROM node:22-slim
WORKDIR /app
COPY package.json .
RUN npm i
COPY . .
ENTRYPOINT ["node", "index.mjs"]
Enter fullscreen mode Exit fullscreen mode

It's basically equivalent to the Python version. The docker command-line instructions to build and run the container are also the same: docker build -qt test . and docker run -p 8000:8080 -t test.

$ docker build -qt test .

sha256:83741dbff139d5bdf09f28cde681e4455828102d75064f99c95708d37d8735e3

$ docker run -p 8000:8080 -t test
Listening on port 8080
Enter fullscreen mode Exit fullscreen mode

After running both commands and starting your container, hit the server at http://localhost:8000. Again, don't expect any Express output on requests. Press ^C to exit as before.

Deploying apps to Cloud Run

Whether via the server or through Docker, you're in good shape to deploy to the cloud if you ran your app locally. The next step is to deploy it to the cloud so that it's accessible globally... let's start with some GCP setup.

📝 Container runtime contract
There are some restrictions your app must adhere to in order for GCR to run your container successfully; the most important pair:

  1. Your app must be stateless. Don't use embedded databases. When your users hit your app again, they may be reaching another instance in a completely different state. Persist data in cloud-based storage like GCS, Cloud SQL, or Cloud Firestore.
  2. Leave a port, say 8080 or another port, open so GCR can reach your app.

A basic web app may have architecture similar to this illustration:
Sample web app running on GCR/GCP

[IMAGE] Sample web app architecture for GCR/GCP

 

For more information, see the Container runtime contract page in the documentation.

Google Cloud SDK

The gcloud command-line tool (CLI) can deploy apps to both GAE and GCR. Unlike GAE however, GCR also lets you deploy apps from the Cloud console once your code or container image are in known places.

The gcloud CLI comes with the Cloud SDK (software development kit). If it's already installed, skip to the next section. New to GCP? Follow the instructions to install the SDK and learn a few gcloud commands. If you just want to install the SDK, see these instructions. After installation, run gcloud init to initialize it.

Google Cloud projects

A GCP project is required to manage all the resources, e.g., APIs, compute, storage, etc., for an app. Create a new one or reuse an existing project. Then assign a billing account to that project if you haven't already. (Review the cost sidebar above for cost details if you missed it earlier.)

Project identifiers

Every project has an ID and a number. (Projects also have names, but that's out-of-scope for this post.) A project ID is a unique string identifier chosen by you at project creation or automatically assigned if you don't specify one; it cannot be updated once a project is created. A project number is a unique numeric identifier automatically assigned by GCP and cannot be updated.

These project identifiers can be found in the Cloud console at https://console.cloud.google.com/iam-admin/settings. You can also run the gcloud projects list to see all identifiers for all projects.

Most of the time, you use a project ID, such as when deploying your service. For your convenience, set the default project ID with this command: gcloud config set project PROJ_ID. If you don't set a default, pass it in with --project PROJ_ID with each gcloud command, else you'll be prompted for it. The project number is used much less frequently, such as in GCR service URLs we'll dive into next.

Service URL(s)

GAE refers to a running service as "apps" with a restriction that all projects can only have one app and only be based in one region. GCR calls apps "services" and has fewer restrictions. GCR can create any number of services, and on top of that, services are deployable to different regions. No such luxury with GAE! Like all GAE apps though, each GCR service comes with a free, default URL, SVC_NAME-PROJ_NUM.REGION.run.app, where:

URL component Description
SVC_NAME Name you chose for your service
PROJ_NUM Project number (not to be confused with project ID)
REGION Region you deployed your service to
[TABLE] GCR free domain name components

 

These deterministic URLs, based on these known components, are predictable enough that when going to production, allow you to map custom domains at them. GCR services have another service URL, less predictable and no longer recommended for use. (See the sidebar below for more details.)

📝 Deterministic vs. non-deterministic service URLs
Prior to 2024, GCR service URLs were not as deterministic: SVC_NAME-HASH-REG_ABBR.run.app. The service name (SVC_NAME) is the same as described above, but rather than a project number, a random HASH value is generated, and instead of a full region name, a somewhat cryptic region abbreviation (REG_ABBR) is used. While these URLs are stable once created, deterministic URLs are more predictable and easier to use. Deterministic URLs became generally available in Sep 2024 and became prioritized at that time.

Set your environment up for deployments

Confirm you've completed these setup tasks:

  1. Install GCP SDK (includes gcloud CLI)
  2. Create new or reuse existing GCP project
  3. Enable billing & assign billing account
  4. Run gcloud init
  5. (optional) Run gcloud config set project PROJ_ID

Build & deploy: single, combination command options

To deploy an app to GAE, you'd use gcloud app deploy. The equivalent for GCR is gcloud run deploy, GCR deploy commands provide more options than GAE, so commands are typically longer. (If they're too long, you can put all build criteria inside a cloudbuild.yaml config file. A realistic deploy command looks like this: gcloud run deploy SVC_NAME --allow-unauthenticated --source . --region REGION

What does it all mean? Let's break it down:

Command-line entity Description/purpose
gcloud run deploy Base command
SVC_NAME Choose a name for your service
--allow-unauthenticated Make this a public service, else it'll be authenticated (restricted by IAM permissions)
--source . Deploy from source code (vs. registered container image -- more on this below)
--region REGION Choose a region to deploy your service to (where you anticipate most of your users are)
[TABLE] GCR deployment command components

 

  • The service name (SVC_NAME) is required.
  • The --allow-unauthenticated flag opens it up globally, making the app easy to try/test; turn on authentication when moving to production (if not a public website)
  • Provide the --region flag and region or else be prompted interactively for it.
  • You can deploy an app: 1) from source code (--source DIRECTORY) or 2) from an already built & registered container (--image IMG_NAME).

Most of the above are self-explanatory but the last pair can do with a bit more explanation:

  1. Region: GAE allows only one app per project which is locked forever to a single region. In sharp contrast, you can deploy any number of GCR services and to any supported region, and regardless of which region it is hosted in, a GCR service is accessible globally.
  2. Source builds: We're deploying this basic app directly from source code, but if you've got a good working build (image) and wish to deploy multiple services, likely to different regions, it's inefficient to build the same container image over and over again. You could also run into inconsistencies if somehow the code changed between those builds.

Build & deploy: multiple, separate commands and options

To avoid the inconsistent build scenarios, "build once and deploy at least once" can be accomplished by splitting up the single, combo gcloud run deploy command from above into a pair, one to build and the other to deploy:

  1. gcloud builds submit --tag pkg.dev/PROJ_ID/REPO/IMG_NAME (Docker) or gcloud builds submit --pack image=pkg.dev/PROJ_ID/REPO/IMG_NAME (Buildpacks)
  2. gcloud run deploy SVC_NAME --image pkg.dev/PROJ_ID/REPO/IMG_NAME

The first command builds a container image and registers it with CAR (*.pkg.dev registry "domain"). (If you run across gcr.io registry "domains", they're from CAR's deprecated predecessor, Container Registry.) The gcloud builds submit command builds a container image and registers it.

Registered container images can be deployed as new services, avoiding repeated builds. If you're familiar with Docker, gcloud builds submit is similar to docker build and docker push while gcloud run deploy would be docker run.

But, if you're just experimenting and don't plan to deploy multiple services with the same image, you can stick with the single combo, all-in-one gcloud run deploy command from the last section.

Deploy to the cloud

Formalities aside, let's deploy these sample apps to GCR. For brevity, I'm sticking with:

  • Source-only deployments (--source .)
  • A set default project ID (else use --project PROJ_ID or be prompted)
  • Allowing unauthenticated traffic (--allow-unauthenticated)
  • The service name of hello
  • The us-west1 region (--region; else be prompted)

These result in the following deployment command: gcloud run deploy hello --allow-unauthenticated --source . --region us-west1

Now I'll use that command to deploy the app in these configurations:

  1. Python with Docker
  2. Node.js with Docker
  3. Node.js (without Docker) with Buildpacks

Pick your own service name and region if you wish; adjust your command-line options accordingly.

Python with Docker

The Dockerfile didn't conflict with the Procfile in my build-and-deploy below but feel free to rename or remove Procfile temporarily if you have issues. Go to the python folder and run the gcloud run deploy command... your output will look similar to:

$ gcloud run deploy hello --allow-unauthenticated --source . --region us-west1
Building using Dockerfile and deploying container to Cloud Run service [hello] in project [PROJ_ID] region [us-west1]
✓ Building and deploying... Done.
  ✓ Uploading sources...
  ✓ Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/builds/c2ab2b1a-cec0-4686-bc77-d6a809fd7c3d?project=PROJ_NUM].
  ✓ Creating Revision...
  ✓ Routing traffic...
  ✓ Setting IAM Policy...
Done.
Service [hello] revision [hello-00004-ds8] has been deployed and is serving 100 percent of traffic.
Service URL: https://hello-PROJ_NUM.us-west1.run.app
Enter fullscreen mode Exit fullscreen mode

Node.js with Docker

If you switching to the nodejs folder, issue the same command:

$ gcloud run deploy hello --allow-unauthenticated --source . --region us-west1
Building using Dockerfile and deploying container to Cloud Run service [hello] in project [PROJ_ID] region [us-west1]
✓ Building and deploying new service... Done.
  ✓ Uploading sources...
  ✓ Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/builds/a4f4ceda-abe8-4116-a30a-8aa5f5cc4279?project=PROJ_NUM].
  ✓ Creating Revision...
  ✓ Routing traffic...
  ✓ Setting IAM Policy...
Done.
Service [hello] revision [hello-00001-9r2] has been deployed and is serving 100 percent of traffic.
Service URL: https://hello-PROJ_NUM.us-west1.run.app
Enter fullscreen mode Exit fullscreen mode

Notice there's no difference to the commands at all: Everything the build system needs is in the Dockerfile. Both result in the app deployed (regardless of language) and available globally at the same URL.

Node.js with Buildpacks

Let's say you're not familiar with Docker or prefer not to use it. Delete, move, or remove Dockerfile and issue the same command... the app also deploys successfully, thanks to Buildpacks:

$ gcloud run deploy hello --allow-unauthenticated --source . --region us-west1
Building using Buildpacks and deploying container to Cloud Run service [hello] in project [PROJ_ID] region [us-west1]
✓ Building and deploying... Done.
  ✓ Uploading sources...
  ✓ Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/builds/0140d524-ecc1-4b97-9079-5695c03d9e01?project=PROJ_NUM].
  ✓ Creating Revision...
  ✓ Routing traffic...
  ✓ Setting IAM Policy...
Done.
Service [hello] revision [hello-00002-z6n] has been deployed and is serving 100 percent of traffic.
Service URL: https://hello-PROJ_NUM.us-west1.run.app
Enter fullscreen mode Exit fullscreen mode

Browse Cloud Build log via the link to see how your service was built and deployed. Our Node.js Dockerfile specifies a Node 22 slim base image, but without it, you're dependent on Buildpacks default runtime versions at build-time. In the last deployment above, Buildpacks used Node 20 because the default hasn't moved up to version 22 yet. So if you're targeting a specific release, using Docker (and Dockerfile) is your best bet.

Globally-accessible and scalable on GCR

For any of the deployments above, hitting the "Service URL" shown at the end of your build command with a browser should look just like your locally-deployed version(s) but are now globally-accessible:

[IMAGE] "Hello World!" sample app running on GCR/GCP

 

Not only are your GAE apps & GCR services available around the world, they autoscale as well. GCP serverless platforms spin up more instances on-demand based on traffic then winds them down as things subside, all without explicit action on your part.

Congrats on successfully deploying the demo app(s) to GCR!

Summary and next steps

Now that you've taken a working Python or Node sample app, tested it locally, and successfully deployed it to GCR/GCP, continue to experiment by updating the code and redeploying, uploading one of your "real" apps, or check out the equivalent GAE post to see how to deploy the same app to that platform. While GCR is newer and has fewer restrictions, that post points out some of the reasons why you may still consider GAE. Regardless of next steps, you now have a new option for running apps in the cloud, and nowhere did you consider allocating, configuring, or paying for a server 24x7.

Billing epilogue

If you want to keep the demo GCR service around, great. Even though it's not a free service, there's a monthly free quota you have to consume in order to incur billing. However, if no one is accessing your app, one of the benefits of serverless is that you're not paying. If someone discovers your app's URL though, or you continue to deploy new versions, this will eventually lead to billable usage. If you're not ready to continue experimenting with GCR or the deployed app, you have several options (from least-to-most "severe"):

Stop GCR service option Description
Disable service Temporarily disable service to avoid incurring additional charges; can be re-enable any time.
Delete service Delete & remove service; cannot be "un-deleted", but you can redeploy again later.
Shutdown project Delete service, billing, and all project resources if you don't want to continue using it with GCR or other GCP services; cannot restore a project 30 days after it has been shut down.

The first pair of options help you avoid charges from a running service, but bear in mind the storage costs for GCS and AR as described earlier may still apply, so delete enough build artifacts to fall under any limits you may be exceeding. The last option "wipes the slate clean" where you no longer have to worry about billing at all.

Other GCR features

GCR has other features worth exploring... subjects of future posts. Preview to get a head start:

GCR feature Description
HTTP & event-driven triggers Services can be event-driven in addition to being "triggered" with HTTP requests; learn other ways to kick off work on GCR
Jobs Run tasks start-to-finish outside of HTTP/S requests, e.g., batch processing; can run for up to 24 hours
Functions Don't have an entire app? Got snippets of code to run that doesn't involve a "full stack"? Functions are great for microservices, mobile backends, etc.

Wrap-up

If you found an error in this post, a bug in the code, or have a topic I should cover, drop a note in the comments below or file an issue at the repo.

If you have older GAE apps and considering a move/port to GCR, want to upgrade from Python 2 to 3 or migrate off GAE bundled services, I may be able to help... check out https://appenginemigrations.com and submit a request there.

I enjoy meeting users on the road... see if I'll be visiting your community in the travel calendar on my consulting page.

References

Below are various resources related to this post which you may find useful.

Blog post code samples

GCR resources & historical references

Other GCR content by the author

  • GCR sample weather alerts app & "always-on CPU" use case post and video
  • GCR sample YouTube comment tracker app & full design use case
    • Part 1: Designing a user interface quickly (frontend/design) video
    • Part 2: Design a serverless architecture (backend/server-side) video
  • Migrate GAE apps to GCR

Other GCP serverless content by the author

  • Explore GCP serverless platforms with a nebulous sample app
    • Part 1: Picking the "right" serverless platform video
    • Part 2: Deploy the same app to App Engine, Cloud Functions, or Cloud Run
      • Blog post, video, and code repo
      • Run Python 2 or 3 sample app locally codelab
      • Run Python 2 sample app on GAE codelab (deprecated: 2.x no longer supported by GAE)
      • Run Python 3 sample app on GAE codelab
      • Run Python 3 sample app on GCF (Gen1) codelab
      • Run Python 2 sample app on GCR with Docker codelab
      • Run Python 3 sample app on GCR with Docker codelab
      • Run Python 3 sample app on GCR without Docker/with Buildpacks codelab
      • Run Node.js sample app on GAE, GCF (Gen1), and GCR codelab
  • How to call Google APIs from GCP serverless platforms post
  • Top 3 pain points for serverless developers video


DISCLAIMER: I was a member of the GCR product team 2020-2023. While product information is as accurate as I can find or recall, the opinions are my own.



WESLEY CHUN, MSCS, is a Google Developer Expert (GDE) in Google Cloud (GCP) & Google Workspace (GWS), author of Prentice Hall's bestselling "Core Python" series, co-author of "Python Web Development with Django", and has written for Linux Journal & CNET. He runs CyberWeb specializing in GCP & GWS APIs and serverless platforms, Python & App Engine migrations, and Python training & engineering. Wesley was one of the original Yahoo!Mail engineers and spent 13+ years on various Google product teams, speaking on behalf of their APIs, producing sample apps, codelabs, and videos for serverless migration and GWS developers. He holds degrees in Computer Science, Mathematics, and Music from the University of California, is a Fellow of the Python Software Foundation, and loves to travel to meet developers worldwide at conferences, user group events, and universities. Follow he/him @wescpy & his technical blog. Find this content useful? Contact CyberWeb for professional services or buy him a coffee (or tea)!

Top comments (0)