How to run a static site in Google Cloud Run

Originally, I wanted to run this site in a Google Cloud Storage bucket. However, I wanted to have more control over the load balancer settings. Specifically, Cloud Storage buckets do not allow HTTPS via a custom domain unless the bucket is fronted by a Google Load Balancer or a third-party CDN.

The simplest solution that also yields a significant amount of control is hosting via Google Cloud Run. Cloud Run is an inexpensive stateless container platform. It performs automatic HTTP to HTTPS redirect (without HSTS, see below) and it's trivial to run a custom NGINX image.

First, follow the Cloud SDK Quickstart guide for your platform.

Configure Docker for Container Registry

Because I am using the fully managed version of Cloud Run, I can only run containers from Cloud Registry images. This is very inexpensive, so I didn't mind.

I did have to configure my local Docker daemon with authentication to push to that registry.

gcloud auth configure-docker

Create a Dockerfile

Create a simple Dockerfile in the root directory that will install Node packages in a builder step before compiling the blog content. This makes caching of each build step much faster.

The NGINX container serves static content with all of the header directives that I wanted to add.

FROM node:13.2-alpine AS builder

COPY package.json package-lock.json /build/
WORKDIR /build
RUN npm install

COPY . /build

ENV NODE_ENV=production

RUN npm run export

FROM nginx:mainline-alpine
COPY --from=builder /build/out /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

It's optional, but I highly recommend using a .dockerignore file to prevent node_modules and other large, unneeded directories from being loaded into the Docker build context.

Create a NGINX configuration

Place an nginx.conf file in the root directory of your project. Be sure to change the server_name directive to match your domain.

server {
    listen 8080;
    server_name yourdomain.com;

    gzip on;
    gzip_types text/html application/javascript text/css;

    expires 1y;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header Content-Security-Policy "default-src 'none'; font-src 'none'; img-src 'self'; object-src 'none'; script-src 'self'; style-src 'self'; frame-ancestors 'none'";
    add_header X-Frame-Options "DENY";
    add_header X-Content-Type-Options "nosniff";

    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
    }

    error_page 404 /404.html;
}

Create and push the Docker image

Create the Docker image, tagging it for storage in Google's Container Registry.

docker build -t gcr.io/PROJECT-ID/image:latest .

Push the image to the registry.

docker push gcr.io/PROJECT-ID/image:latest

Deploy to Cloud Run

Deploy the new image to Cloud Run. Substitute your service name and the image name created above.

gcloud run deploy SERVICE-NAME --image gcr.io/PROJECT-ID/image:latest

If you want the site to be publicly accessible, say "y" when asked if you want to allow unauthenticated invocations.

Once the deploy is complete, the application URL will be outputted. You can set up a CNAME record to that URL or map a custom domain to that service.