Many Google Cloud Run users are starting to develop containers for the first time, but often they are migrating their existing applications. Sometimes, these apps aren’t designed as microservices that fit one-process-per-container model, and require multiple server processes running together in a container.

Often you will hear “running multiple processes in a container is bad”, although nothing is wrong with doing so, as I explained in my previous article comparing init systems optimized for containers.

In this article, I’ll show a not super production-ready (hence “the lazy way”) but working solution for running multi-process containers on Cloud Run, and will provide example code.

Example Use Cases

Coming to Cloud Run as a new user, you might need to run multiple processes in a container for all sorts of reasons like:

  • reverse-proxy in front of your app (like nginx/Envoy or Cloud Endpoints ESP)
  • a non-HTTP server exposed via the primary HTTP server (like grpc-web)
  • local proxy server for outbound traffic
  • instance-local caches (e.g. memcached)
  • other examples of cooperating processes

Designing the container

In this article, I’ll use the nginx example. We want to develop a container image that runs nginx server, as well as your server app.

We will run nginx on main Cloud Run port (8080), and your server on 8081 as nginx will route traffic to your application like:

  • /static/* paths are served by nginx from local filesystem
  • /* anything else is forwarded to your main server process

Every container must have a single entrypoint process. When entrypoint exits, which the container dies. Therefore, you still need a single “entrypoint script” to start both servers (nginx and your app).

If one of these servers terminate (indicating something is bad), your entrypoint script should also terminate quickly instead of trying to revive the server processes (as the container will be restarted easily anyway).

diagram showing container with a script as the entrypoint starting nginx anduser application

This entrypoint script is not so much of a “nanny process” that supervises the subprocesses and restarts them. It rather waits for one of them to terminate, so we can fail fast by exiting the container.

In this example, you write a bash script to be the entrypoint of your container that starts and waits for the actual servers you want to run:

#!/usr/bin/env bash
set -e

nginx &
env PORT=8081 /path/to/your-app &

wait -n
  • & makes the process run in the background (but it still as a child process of this script)
  • wait -n (introduced in bash 4.3) returns as soon as a background process exits (indicating an issue with server processes) so we can terminate the container.
  • env PORT=... command overrides the $PORT entrypoint for your actual app, as the Cloud Run PORT=8080 will be used by nginx.

bash: A proper init process?

Entrypoint process of a Linux container runs with PID (process id) 1, which has special responsibilities, such as (but not limited to):

  • forwarding signals to the child processes
  • handling what to do with a died indirect child process is parented to it (a.k.a. zombie child reaping problem)

When we make our entrypoint a bash script, it can’t do these things. Since Cloud Run apps are meant to be stateless (and are be killed at any time ungracefully), we don’t care as much about signal forwarding behavior.

However, zombie child reaping can be a problem, especially when you run programs (like nginx) that start a lot of short-lived child processes as their children will get parented to the entrypoint.

To handle these situations, you can insert a small init process like tini to be the new entrypoint of your container and start the script from there (as tini still can’t supervise multiple children that we need):

diagram showing container with tini as the entrypoint supervising a scriptthat starts nginx and userapplication

In this case, tini helps you reap the child processes. Refer to the Dockerfile in the repository to see how tini integrated as the new ENTRYPOINT.

Try it out

As promised earlier, here’s a repository containing the Dockerfile and source code of the container with NGINX that serves static assets on /static/* and proxies everything else to a Python server.

Once you deploy this container image to Cloud Run, you get a container serving traffic from multiple processes running in a single container.

You can read my previous article on comparison of init systems for containers to learn more about other options you have for running multiple processes in containers.

Hope this helps you migrate your existing or legacy applications to Cloud Run and take advantage of serverless containers. In the next blog post, I will explore using an actual init process (instead of bash) to supervise the sub-processes.