Serve dynamic content and host microservices with Cloud Run

Pair Cloud Run with Firebase Hosting to generate and serve your dynamic content or build REST APIs as microservices.

Using Cloud Run, you can deploy an application packaged in a container image. Then, using Firebase Hosting, you can direct HTTPS requests to trigger your containerized app.

  • Cloud Run supports several languages (including Go, Node.js, Python, and Java), giving you the flexibility to use the programming language and framework of your choice.
  • Cloud Run automatically and horizontally scales your container image to handle the received requests, then scales down when demand decreases.
  • You only pay for the CPU, memory, and networking consumed during request handling.

For example use cases and samples for Cloud Run integrated with Firebase Hosting, visit our serverless overview.


This guide shows you how to:

  1. Write a simple Hello World application
  2. Containerize an app and upload it to Artifact Registry
  3. Deploy the container image to Cloud Run
  4. Direct Hosting requests to your containerized app

Note that to improve the performance of serving dynamic content, you can optionally tune your cache settings.

Before you begin

Before using Cloud Run, you need to complete some initial tasks, including setting up a Cloud Billing account, enabling the Cloud Run API, and installing the gcloud command line tool.

Set up billing for your project

Cloud Run offers free usage quota, but you still must have a Cloud Billing account associated with your Firebase project to use or try out Cloud Run.

Enable the API and install the SDK

  1. Enable the Cloud Run API in the Google APIs console:

    1. Open the Cloud Run API page in the Google APIs console.

    2. When prompted, select your Firebase project.

    3. Click Enable on the Cloud Run API page.

  2. Install and initialize the Cloud SDK.

  3. Check that the gcloud tool is configured for the correct project:

    gcloud config list

Step 1: Write the sample application

Note that Cloud Run supports many other languages in addition to the languages shown in the following sample.

Go

  1. Create a new directory named helloworld-go, then change directory into it:

    mkdir helloworld-go
    cd helloworld-go
  2. Create a new file named helloworld.go, then add the following code:

    package main
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    )
    
    func handler(w http.ResponseWriter, r *http.Request) {
    	log.Print("helloworld: received a request")
    	target := os.Getenv("TARGET")
    	if target == "" {
    		target = "World"
    	}
    	fmt.Fprintf(w, "Hello %s!\n", target)
    }
    
    func main() {
    	log.Print("helloworld: starting server...")
    
    	http.HandleFunc("/", handler)
    
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    	}
    
    	log.Printf("helloworld: listening on port %s", port)
    	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
    }
    

    This code creates a basic web server that listens on the port defined by the PORT environment variable.

Your app is finished and ready to be containerized and uploaded to Artifact Registry.

Node.js

  1. Create a new directory named helloworld-nodejs, then change directory into it:

    mkdir helloworld-nodejs
    cd helloworld-nodejs
  2. Create a package.json file with the following contents:

    {
      "name": "knative-serving-helloworld",
      "version": "1.0.0",
      "description": "Simple hello world sample in Node",
      "main": "index.js",
      "scripts": {
        "start": "node index.js"
      },
      "author": "",
      "license": "Apache-2.0",
      "dependencies": {
        "express": "^4.21.1"
      }
    }
    
  3. Create a new file named index.js, then add the following code:

    const express = require('express');
    const app = express();
    
    app.get('/', (req, res) => {
      console.log('Hello world received a request.');
    
      const target = process.env.TARGET || 'World';
      res.send(`Hello ${target}!\n`);
    });
    
    const port = process.env.PORT || 8080;
    app.listen(port, () => {
      console.log('Hello world listening on port', port);
    });
    

    This code creates a basic web server that listens on the port defined by the PORT environment variable.

Your app is finished and ready to be containerized and uploaded to Artifact Registry.

Python

  1. Create a new directory named helloworld-python, then change directory into it:

    mkdir helloworld-python
    cd helloworld-python
  2. Create a new file named app.py, then add the following code:

    import os
    
    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route('/')
    def hello_world():
        target = os.environ.get('TARGET', 'World')
        return 'Hello {}!\n'.format(target)
    
    if __name__ == "__main__":
        app.run(debug=True,host='0.0.0.0',port=int(os.environ.get('PORT', 8080)))
    

    This code creates a basic web server that listens on the port defined by the PORT environment variable.

Your app is finished and ready to be containerized and uploaded to Artifact Registry.

Java

  1. Install Java SE 8 or later JDK and CURL.

    Note that we only need to do this to create the new web project in the next step. The Dockerfile, which is described later, will load all dependencies into the container.

  2. From the console, create a new empty web project using cURL then unzip commands:

    curl https://start.spring.io/starter.zip \
        -d dependencies=web \
        -d name=helloworld \
        -d artifactId=helloworld \
        -o helloworld.zip
    unzip helloworld.zip

    This creates a SpringBoot project.

  3. Update the SpringBootApplication class in src/main/java/com/example/helloworld/HelloworldApplication.java by adding a @RestController to handle the / mapping and also add a @Value field to provide the TARGET environment variable:

    package com.example.helloworld;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @SpringBootApplication
    public class HelloworldApplication {
    
      @Value("${TARGET:World}")
      String target;
    
      @RestController
      class HelloworldController {
        @GetMapping("/")
        String hello() {
          return "Hello " + target + "!";
        }
      }
    
      public static void main(String[] args) {
        SpringApplication.run(HelloworldApplication.class, args);
      }
    }
    

    This code creates a basic web server that listens on the port defined by the PORT environment variable.

Your app is finished and ready to be containerized and uploaded to Artifact Registry.

Step 2: Containerize an app and upload it to Artifact Registry

  1. Containerize the sample app by creating a new file named Dockerfile in the same directory as the source files. Copy the following content into your file.

    Go

    # Use the official Golang image to create a build artifact.
    # This is based on Debian and sets the GOPATH to /go.
    FROM golang:latest AS builder
    
    ARG TARGETOS
    ARG TARGETARCH
    
    # Create and change to the app directory.
    WORKDIR /app
    
    # Copy local code to the container image.
    COPY . ./
    
    # Install dependencies and tidy up the go.mod and go.sum files.
    RUN go mod tidy
    
    # Build the binary.
    # -mod=readonly ensures immutable go.mod and go.sum in container builds.
    RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -mod=readonly -v -o server
    
    # Use the official Alpine image for a lean production container.
    # https://hub.docker.com/_/alpine
    # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
    FROM alpine:3
    RUN apk add --no-cache ca-certificates
    
    # Copy the binary to the production image from the builder stage.
    COPY --from=builder /app/server /server
    
    # Run the web service on container startup.
    CMD ["/server"]
    

    Node.js

    # Use the official lightweight Node.js 12 image.
    # https://hub.docker.com/_/node
    FROM node:12-slim
    
    # Create and change to the app directory.
    WORKDIR /usr/src/app
    
    # Copy application dependency manifests to the container image.
    # A wildcard is used to ensure both package.json AND package-lock.json are copied.
    # Copying this separately prevents re-running npm install on every code change.
    COPY package*.json ./
    
    # Install production dependencies.
    RUN npm install --only=production
    
    # Copy local code to the container image.
    COPY . ./
    
    # Run the web service on container startup.
    CMD [ "npm", "start" ]
    

    Python

    # Use the official lightweight Python image.
    # https://hub.docker.com/_/python
    FROM python:3.7-slim
    
    # Allow statements and log messages to immediately appear in the Knative logs
    ENV PYTHONUNBUFFERED True
    
    # Copy local code to the container image.
    ENV APP_HOME /app
    WORKDIR $APP_HOME
    COPY . ./
    
    # Install production dependencies.
    RUN pip install Flask gunicorn
    
    # Run the web service on container startup. Here we use the gunicorn
    # webserver, with one worker process and 8 threads.
    # For environments with multiple CPU cores, increase the number of workers
    # to be equal to the cores available.
    CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app
    

    Java

    # Use the official maven/Java 8 image to create a build artifact: https://hub.docker.com/_/maven
    FROM maven:3.5-jdk-8-alpine AS builder
    
    # Copy local code to the container image.
    WORKDIR /app
    COPY pom.xml .
    COPY src ./src
    
    # Build a release artifact.
    RUN mvn package -DskipTests
    
    # Use the Official OpenJDK image for a lean production stage of our multi-stage build.
    # https://hub.docker.com/_/openjdk
    # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
    FROM openjdk:8-jre-alpine
    
    # Copy the jar to the production image from the builder stage.
    COPY --from=builder /app/target/helloworld-*.jar /helloworld.jar
    
    # Run the web service on container startup.
    CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/helloworld.jar"]
    
  2. Build your container image using Cloud Build by running the following command from the directory containing your Dockerfile:

    gcloud builds submit --tag gcr.io/PROJECT_ID/helloworld

    Upon success, you will see a SUCCESS message containing the image name
    (gcr.io/PROJECT_ID/helloworld).

The container image is now stored in Artifact Registry and can be re-used if desired.

Note that, instead of Cloud Build, you can use a locally installed version of Docker to build your container locally.

Step 3: Deploy the container image to Cloud Run

For the best performance, colocate your Cloud Run service with Hosting using the following regions:

  • us-west1
  • us-central1
  • us-east1
  • europe-west1
  • asia-east1

Rewrites to Cloud Run from Hosting are supported in the following regions:

  • asia-east1
  • asia-east2
  • asia-northeast1
  • asia-northeast2
  • asia-northeast3
  • asia-south1
  • asia-south2
  • asia-southeast1
  • asia-southeast2
  • australia-southeast1
  • australia-southeast2
  • europe-central2
  • europe-north1
  • europe-southwest1
  • europe-west1
  • europe-west12
  • europe-west2
  • europe-west3
  • europe-west4
  • europe-west6
  • europe-west8
  • europe-west9
  • me-central1
  • me-west1
  • northamerica-northeast1
  • northamerica-northeast2
  • southamerica-east1
  • southamerica-west1
  • us-central1
  • us-east1
  • us-east4
  • us-east5
  • us-south1
  • us-west1
  • us-west2
  • us-west3
  • us-west4
  • us-west1
  • us-central1
  • us-east1
  • europe-west1
  • asia-east1

  1. Deploy using the following command:

    gcloud run deploy --image gcr.io/PROJECT_ID/helloworld
  2. When prompted:

  3. Wait a few moments for the deploy to complete. On success, the command line displays the service URL. For example: https://helloworld-RANDOM_HASH-us-central1.a.run.app

  4. Visit your deployed container by opening the service URL in a web browser.

The next step walks you through how to access this containerized app from a Firebase Hosting URL so that it can generate dynamic content for your Firebase-hosted site.

Step 4: Direct hosting requests to your containerized app

With rewrite rules, you can direct requests that match specific patterns to a single destination.

The following example shows how to direct all requests from the page /helloworld on your Hosting site to trigger the startup and running of your helloworld container instance.

  1. Make sure that:

    For detailed instructions about installing the CLI and initializing Hosting, see the Get Started guide for Hosting.

  2. Open your firebase.json file.

  3. Add the following rewrite configuration under the hosting section:

    "hosting": {
      // ...
    
      // Add the "rewrites" attribute within "hosting"
      "rewrites": [ {
        "source": "/helloworld",
        "run": {
          "serviceId": "helloworld",  // "service name" (from when you deployed the container image)
          "region": "us-central1",    // optional (if omitted, default is us-central1)
          "pinTag": true              // optional (see note below)
        }
      } ]
    }
    
  4. Deploy your hosting configuration to your site by running the following command from the root of your project directory:

    firebase deploy --only hosting

With this feature, you can ensure that the revision of your Cloud Run service for generating your site's dynamic content is kept in sync with your static Hosting resources and Hosting config. Also, this feature allows you to preview your rewrites to Cloud Run on Hosting preview channels.

If you add "pinTag": true to a run block of the hosting.rewrites config, your static Hosting resources and configuration will be pinned to the most recent revision of the Cloud Run service, at the time of deploy. If you roll back a version of your site, the revision of the "pinned" Cloud Run service is also rolled back.

This feature relies on Cloud Run tags, which have a limit of 1000 tags per service and 2000 tags per region. This means that after hundreds of deploys, the oldest versions of a site may stop working.

Your container is now reachable via the following URLs:

  • Your Firebase subdomains:
    PROJECT_ID.web.app/ and PROJECT_ID.firebaseapp.com/

  • Any connected custom domains:
    CUSTOM_DOMAIN/

Visit the Hosting configuration page for more details about rewrite rules. You can also learn about the priority order of responses for various Hosting configurations.

Test locally

During development, you can run and test your container image locally. For detailed instructions, visit the Cloud Run documentation.

Next steps