Google Cloud Run, Strapi with PostgreSQL on Google Cloud Platform

Ganesh, Mohan
Google Cloud - Community
10 min readJan 1, 2023

I had to find a reliable way to host Strapi with PostgresSQL at a reasonable cost for my projects because Heroku stopped allowing users to run PostgreSQL for free. Strapi is the next-gen headless CMS, open-source, javascript, enabling content-rich experiences to be created, managed and exposed to any digital device.

In this post, we’ll implement a secure, scaleable hosting solution for Strapi that uses PostgreSQL for persistence. We will be using the Google Cloud Platform to run these services. Strapi version is 4.5.4 and PostgreSQL is 14 version. Even though we refer to this as self-hosting, this is merely a one-time setup, and we would be using the infrastructure of the serverless kind, meaning that we wouldn’t be in charge of any aspects of infrastructure management.

Google Cloud Products

The following products will be needed to host the Strapi on the Google Cloud Platform.

Cloud Run: A fully managed compute platform that automatically scales containers. Build apps in your favorite language and deploy them in seconds.

PostgreSQL: PostgreSQL is the leading open-source relational database with an active and growing ecosystem of developers and tools.

VPC Network: A Virtual Private Cloud (VPC) network is a virtual version of a physical network, implemented inside of Google’s production network.

VPC Network Peering: Google Cloud VPC Network Peering connects two Virtual Private Cloud (VPC) networks so that resources in each network can communicate with each other.

Serverless VPC Access: Serverless VPC Access makes it possible for you to connect directly to your Virtual Private Cloud network from serverless environments such as Cloud Run, App Engine, or Cloud Functions. Serverless VPC Access is based on a resource called a connector. A connector handles traffic between your serverless environment and your VPC network. When you create a connector in your Google Cloud project, you attach it to a specific VPC network and region. You can then configure your serverless services to use the connector for outbound network traffic.

Google Cloud Build: Cloud Build scales up and down with no infrastructure to set up, upgrade, or scale. Run builds in a fully managed environment in Google Cloud with connectivity to any of the third-party source providers.

We are going to break down this exercise into multipart.

Goal for this tutorial, we’ll create the required services and resources, build the application, and construct it locally and deploy Google Cloud Run to allow for quick validation. Follow the steps from part 2 of this article to build and deploy in continuous integrate and deploy way.

First, we will set up the VPC network. This VPC network will be attached to PostgreSQL so that we can connect through the Google internal network.

Second, a serverless connector will be created and connected to the above-created VPC network.

Third, the Strapi application will be deployed on Cloud Run and use the serverless connector to connect to PostgreSQL in a secure way.

Design

Let's enable the possible list of services, that will be needed for us

On your terminal export the GCP project id to the PROJECT_ID variable, this will be handy for further gcloud commands.

export PROJECT_ID=<your-project-id>
export REGION=<your-preferred-region>
gcloud config set project $PROJECT_ID

Enable the services

gcloud services enable \
compute.googleapis.com \
cloudbuild.googleapis.com \
containerregistry.googleapis.com \
sql-component.googleapis.com \
storage-component.googleapis.com \
servicenetworking.googleapis.com \
sqladmin.googleapis.com

Set up the Virtual Private Cloud

gcloud compute networks create vpc-sample-network \
--subnet-mode=auto \
--bgp-routing-mode=regional \
--mtu=1460 \
--description='Sample VPC Network' \
--project=$PROJECT_ID

Here, we’re creating a VPC network with the name vpc-sample-network and auto-created subnets. Upon successful execution of the command , the output message would look something like this. Ignore the firewall rules for this exercise. The auto-created subnets may not be encouraged in an enterprise, but for this exercise, we’ll use the auto creation method.

Created [https://www.googleapis.com/compute/v1/projects/<your-project-id>/global/networks/vpc-sample-network].
NAME SUBNET_MODE BGP_ROUTING_MODE IPV4_RANGE GATEWAY_IPV4
vpc-sample-network AUTO REGIONAL

Instances on this network will not be reachable until firewall rules
are created. As an example, you can allow all internal traffic between
instances as well as SSH, RDP, and ICMP by running:

$ gcloud compute firewall-rules create <FIREWALL_NAME> --network vpc-strapi-network --allow tcp,udp,icmp --source-ranges <IP_RANGE>
$ gcloud compute firewall-rules create <FIREWALL_NAME> --network vpc-strapi-network --allow tcp:22,tcp:3389,icmp

Next, Create an Internal IP address and make sure the purpose is VPC_PEERING.

gcloud compute addresses create google-managed-ip-vpc-sample-network \
--global \
--prefix-length=20 \
--purpose=VPC_PEERING \
--network=vpc-sample-network \
--project=$PROJECT_ID

The above command allocates ip for the VPC network for the purpose of VPC Peering. Upon addresses being reserved, you would see an output message like below

gcloud compute addresses create google-managed-ip-vpc-sample-network \
> --global \
> --purpose=VPC_PEERING \
> --prefix-length=20 \
> --description="peering google ip-range-vpc-network" \
> --network=vpc-sample-network \
> --project=$PROJECT_ID
Created [https://www.googleapis.com/compute/v1/projects/<your-project-id>/global/addresses/google-managed-ip-vpc-sample-network].

Next, Create the vpc-peering service of the type service networking.googleapis.com This is Google-offered internal networking. Here we are binding the vpc-sample-network we have created with the internal IP address we have reserved, using Google networking services.

gcloud services vpc-peerings connect \
--service=servicenetworking.googleapis.com \
--ranges=google-managed-ip-vpc-sample-network \
--network=vpc-sample-network \
--project=$PROJECT_ID

This step connects the VPC network with the internal ip address using google service networking.

gcloud services vpc-peerings connect \
> --service=servicenetworking.googleapis.com \
> --ranges=google-managed-service-ip-vpc-network \
> --network=vpc-sample-network \
> --project=$PROJECT_ID
Operation "operations/pssn.p24-1086939497593-aef550ce-821b-41e1-baba-7ff95217fc5b" finished successfully.

It's about time to create PostgreSQL. Here, I’ve chosen extremely basic CPU and memory configurations, but you can adjust them to meet your demands for CPU, memory, and storage.

gcloud beta sql instances create strapi-postgres-v14 \
--database-version=POSTGRES_14 \
--cpu=1 --memory=3840MB \
--region=us-central1 \
--network=vpc-sample-network \
--no-assign-ip \
--storage-size=10GB \
--enable-google-private-path

Upon successful execution of the command, message like below would be displayed. The database can be reached only from the network ‘vpc-sample-network’

 gcloud beta sql instances create strapi-postgres-v14 \
> --database-version=POSTGRES_14 \
> --cpu=1 \
> --memory=3072MB \
> --region=us-central1 \
> --network=projects/$PROJECT_ID/global/networks/vpc-sample-network \
> --no-assign-ip \
> --storage-size=10GB \
> --enable-google-private-path
Creating Cloud SQL instance for POSTGRES_14...done.
Created [https://sqladmin.googleapis.com/sql/v1beta4/projects/<your-project-id>/instances/strapi-postgres-v14].
NAME DATABASE_VERSION LOCATION TIER PRIMARY_ADDRESS PRIVATE_ADDRESS STATUS
strapi-postgres12 POSTGRES_12 us-central1-f db-custom-2-7680 - 10.100.80.3 RUNNABLE

Here are a few things to notice when you create the database. We connected the VPC network to the database and added the options -no-assign-ip (no public ip) and enable Google Private Path so that we can only access the database in secure internal ways alone. Also, make a note of the internal IP address (PRIVATE_ADDRESS).

Let’s build a new database for the Strapi application following the creation of the database.

gcloud sql databases create strapi --instance=strapi-postgres-v14

gcloud sql databases create strapi \
> --instance=strapi-postgres-v14
Creating Cloud SQL database...done.
Created database [strapi].
instance: strapi-postgres14
name: strapi

Also, create a new database user strapi to use in the Strapi application. Make sure you remember this password, as you would need to provide this to the Strapi application. Provide your own password by replacing ******

gcloud sql users create strapi --instance=strapi-postgres-v14 --password=******

gcloud sql users create strapi \
> --instance=strapi-postgres-v14 \
> --password=*****
Creating Cloud SQL user...done.
Created user [strapi]

Serverless Connector You can use a Serverless VPC Access connector to connect your serverless environment directly to your Virtual Private Cloud (VPC) network, allowing access to Compute Engine virtual machine (VM) instances, Memorystore instances, and any other resources with an internal IP address. Since we will be using Cloud Run to host Strapi, we will need to create a Serverless connector, which utilizes the vpc-sample-network to connect to the database which is available only on the internal network.

gcloud compute networks vpc-access connectors create sample-servless-connector \
--network=vpc-sample-network \
--region=us-central1 \
--range=10.9.0.0/28 \
--min-instances=2 \
--max-instances=3 \
--machine-type=f1-micro

The prerequisite for hosting the Strapi application is accomplished by the VPC, PostgreSQL, and Serverless Connector creation steps. Next, we will begin to set up the Strapi application

You can either use the git repo https://github.com/mohan-ganesh/strapi or use the terminal to create the strapi application.

npx create-strapi-app@4-5-4 --quickstart

If you are building a project using the terminal, change the working directory to the project directory and add the PG module. This is Postgres client for Node to connect

npm install pg --save

In the below, prompts are followed for custom

npx create-strapi-app@v4.5.4 strapi-454
Need to install the following packages:
create-strapi-app@4.5.4
Ok to proceed? (y) y
? Choose your installation type Custom (manual settings)
? Choose your preferred language TypeScript
? Choose your default database client postgres
? Database name: strapi
? Host: 10.100.80.3
? Port: 5432
? Username: strapi
? Password: *********
? Enable SSL connection: No

Creating a project with custom database options.
Creating a new Strapi application at /Users/<user>/projects/strapi/strapi-454.
Creating files.
Dependencies installed successfully.

Your application was created at /Users/<user>/projects/strapi/strapi-454.

Available commands in your project:

yarn develop
Start Strapi in watch mode. (Changes in Strapi project files will trigger a server restart)

yarn start
Start Strapi without watch mode.

yarn build
Build Strapi admin panel.

yarn strapi
Display all available commands.

You can start by doing:

cd /Users/<user>/projects/strapi/strapi-454
yarn develop

npm notice
npm notice New major version of npm available! 8.15.0 -> 9.2.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v9.2.0
npm notice Run npm install -g npm@9.2.0 to update!
npm notice

We will need to make a few adjustments to the strapi project after it is created or cloned. We will first create and deploy the application to the cloud using local environment settings, lets use the local branch if you are using the git repo. Knowing that everything is in place will help. Following this quick test, we’ll utilize Google Cloud Build to build and deploy from a specified branch, injecting all the credentials using cloud build variables.

Edit the /config/database.ts file with the below changes, and all of the environment variables are read from .env file for the initial shakeout.

export default ({ env }) => ({
connection: {
client: 'postgres',
connection: {
host: env('DATABASE_HOST'),
port: env.int('DATABASE_PORT', 5432),
database: env('DATABASE_NAME'),
user: env('DATABASE_USERNAME'),
password: env('DATABASE_PASSWORD'),
ssl: env.bool('DATABASE_SSL', false),
},
debug: true,
acquireConnectionTimeout: 1000000,
options: {
pool: {
min: 1,
max: 5,
acquireTimeoutMillis: 900000,
createTimeoutMillis: 900000,
destroyTimeoutMillis: 900000,
}
},

},
});

Some of the additional database attributes are to address the Knex timeout errors.

Knex: Timeout acquiring a connection. The pool is probably full. Are you missing a .transacting(trx) call?

Add the following additional attributes to the .env file

HOST=0.0.0.0
PORT=1337
APP_KEYS=<default generated keys>
API_TOKEN_SALT=<default generated salt>
ADMIN_JWT_SECRET=<default generated secret>
JWT_SECRET=<default jwt>
DATABASE_HOST=<internal-ip-address-postgres-database>
DATABASE_PORT=5432
DATABASE_NAME=<new-database-name>
DATABASE_USERNAME=
DATABASE_PASSWORD=

Once all of these edits are in place, we are ready to compile and generate the Docker image.

gcloud builds submit --tag gcr.io/$PROJECT_ID/strapi-image-local

gcloud builds submit --tag gcr.io/$PROJECT_ID/strapi-image-local
Creating temporary tarball archive of 23 file(s) totalling 465.7 KiB before compression.
Some files were not included in the source upload.

Check the gcloud log [/tmp/tmp.0q262TF3Zl/logs/2022.12.31/19.55.32.106739.log] to see which files and the contents of the
default gcloudignore file used (see `$ gcloud topic gcloudignore` to learn
more).

The above command submits the build command to Google Cloud Build. Upon the success of the build we should have a Docker image being generated and stored to Container Registry

Now we are ready to deploy the image to Cloud Run. Let's create a dedicated service account and add the necessary permission for it..

gcloud iam service-accounts create strapi \
--description="Service Account for Strapi service" \
--display-name="Strapi"

Add the permission Cloud Run Invoker and Service Account User and deploy the Docker Image to Cloud Run

gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:strapi@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/run.invoker"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:strapi@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/iam.serviceAccountUser"
gcloud run deploy strapi --image gcr.io/$PROJECT_ID/strapi-image-local \
--vpc-connector=sample-servless-connector \
--region=us-central1 \
--vpc-egress=private-ranges-only \
--allow-unauthenticated \
--service-account=strapi

To test the changes from local build for quick validation we are deploying the application having to be able to access from the public internet ( — allow-unauthenticated). For production set up, we should bind the cloud run Strapi service to Google Load Balancer using Network Endpoint Groups.

gcloud run deploy strapi --image gcr.io/$PROJECT_ID/strapi-image-local --vpc-connector=sample-servless-connector --region=us-central1 --vpc-egress=private-ranges-only --no-allow-unauthenticated --service-account=strapi
Deploying container to Cloud Run service [strapi] in project [your-project-id] region [us-central1]
OK Deploying... Done.
OK Creating Revision...
OK Routing traffic...
OK Setting IAM Policy...
Done.
Service [strapi] revision [strapi-00002-bub] has been deployed and is serving 100 percent of traffic.
Service URL: https://strapi-fliophqv7q-uc.a.run.app

Upon successfully deploy of the Strapi application to Cloud Run, check the logs. You should see an output log statements like below.

These outputs confirm we have successfully deployed the Strapi application. Upon access of the Service Url you would see like the below screen

Next, we will build and deploy through Google Cloud Build by connecting Google Cloud Build to the Git repo at Part 2

If you found this information to be useful, a clap would be appreciated. A follow would be even more motivating to write more.
Be joyful because we mostly exist in our minds. keep smiling and be healthy!

--

--