MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

AlloyDB Agentic RAG Application with MCP Toolbox [Part 2]

2025-11-26 05:18:50

This is Part 2 of the AlloyDB Agentic RAG application tutorial, please start with Part 1.

7. Deploy the MCP Toolbox to Cloud Run

Now we can deploy the MCP Toolbox to Cloud Run. There are different ways how the MCP toolbox can be deployed. The simplest way is to run it from the command line but if we want to have it as a scalable and reliable service then Cloud Run is a better solution.

Prepare Client ID

To use booking functionality of the application we need to prepare OAuth 2.0 Client ID using Cloud Console. Without it we cannot sign into the application with our Google credentials to make a booking and record the booking to the database.

In the Cloud Console go to the APIs and Services and click on "OAuth consent screen". Here is a link to the page. It will open the Oauth Overview page where we click Get Started.

On the next page we provide the application name, user support email and click Next.

On the next screen we choose Internal for our application and click Next again.

Then again we provide contact email and click Next

Then we agree with Google API services policies and push the Create button.

It will lead us to the page where we can create an OAuth client.

On the screen we choose "Web Application" from the dropdown menu, put "Cymbal Air" as application and push the Add URI button.

The URIs represent trusted sources for the application and they depend on where you are trying to reach the application from. We put "http://localhost:8081" as authorized URI and "http://localhost:8081/login/google" as redirect URI. Those values would work if you put in your browser "http://localhost:8081" as a URI for connection. For example, when you connect through an SSH tunnel from your computer for example. I will show you how to do it later.

After pushing the "Create" button you get a popup window with your clients credentials. And the credentials will be recorded in the system. You always can copy the client ID to be used when you start your application.

Later you will see where you provide that client ID.

Create Service Account

We need a dedicated service account for our Cloud Run service with all required privileges. For our service we need access to AlloyDB and Cloud Secret Manager. As for the name for the service account we are going to use toolbox-identity.

Open another Cloud Shell tab using the sign "+" at the top.

In the new cloud shell tab execute:

export PROJECT_ID=$(gcloud config get-value project)
gcloud iam service-accounts create toolbox-identity

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:toolbox-identity@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/alloydb.client"
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:toolbox-identity@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/serviceusage.serviceUsageConsumer"
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:toolbox-identity@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor"

Please pay attention if you have any errors. The command is supposed to create a service account for cloud run service and grant privileges to work with secret manager, database and Vertex AI.

Close the tab by either pressing ctrl+d or executing command "exit" in the tab:

exit

Prepare MCP Toolbox Configuration

Prepare configuration file for the MCP Toolbox. You can read about all configuration options in the documentation but here we are going to use the sample tools.yaml file and replace some values such as cluster and instance name, AlloyDB password and the project id by our actual values.

Export AlloyDB Password:

export PGPASSWORD=<noted AlloyDB password>

Export client ID we prepared in the previous step:

export CLIENT_ID=<noted OAuth 2.0 client ID for our application>

Prepare configuration file.

PROJECT_ID=$(gcloud config get-value project)
ADBCLUSTER=alloydb-aip-01
sed -e "s/project: retrieval-app-testing/project: $(gcloud config get-value project)/g" \
-e "s/cluster: my-alloydb-cluster/cluster: $ADBCLUSTER/g" \
-e "s/instance: my-alloydb-instance/instance: $ADBCLUSTER-pr/g" \
-e "s/password: postgres/password: $PGPASSWORD\\n    ipType: private/g" \
-e "s/^ *clientId: .*/    clientId: $CLIENT_ID/g" \
cymbal-air-toolbox-demo/tools.yaml >~/tools.yaml

If you look into the file section defining the target data source you will see that we also added a line to use private IP for connection.

sources:
  my-pg-instance:
    kind: alloydb-postgres
    project: gleb-test-short-003-471020
    region: us-central1
    cluster: alloydb-aip-01
    instance: alloydb-aip-01-pr
    database: assistantdemo
    user: postgres
    password: L23F...
    ipType: private
authServices:
  my_google_service:
    kind: google
    clientId: 96828*******-***********.apps.googleusercontent.com

Create a secret using the tools.yaml configuration as a source.

In the VM ssh console execute:

gcloud secrets create tools --data-file=tools.yaml

Expected console output:

student@instance-1:~$ gcloud secrets create tools --data-file=tools.yaml
Created version [1] of the secret [tools].

Deploy the MCP Toolbox as a Cloud Run Service

Now everything is ready to deploy the MCP Toolbox as a service to Cloud Run. For local testing you can run "./toolbox –tools-file=./tools.yaml" but if we want our application to run in the cloud the deployment in Cloud Run makes much more sense.

In the VM SSH session execute:

export IMAGE=us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:latest
gcloud run deploy toolbox \
    --image $IMAGE \
    --service-account toolbox-identity \
    --region us-central1 \
    --set-secrets "/app/tools.yaml=tools:latest" \
    --args="--tools-file=/app/tools.yaml","--address=0.0.0.0","--port=8080" \
    --network default \
    --subnet default \
    --no-allow-unauthenticated

Expected console output:

student@instance-1:~$ export IMAGE=us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:latest
gcloud run deploy toolbox \
    --image $IMAGE \
    --service-account toolbox-identity \
    --region us-central1 \
    --set-secrets "/app/tools.yaml=tools:latest" \
    --args="--tools-file=/app/tools.yaml","--address=0.0.0.0","--port=8080" \
    --network default \
    --subnet default \
    --no-allow-unauthenticated
Deploying container to Cloud Run service [toolbox] in project [gleb-test-short-002-470613] region [us-central1]
✓ Deploying new service... Done.                                                                                                                                                                                                
  ✓ Creating Revision...                                                                                                                                                                                                        
  ✓ Routing traffic...                                                                                                                                                                                                          
Done.                                                                                                                                                                                                                           
Service [toolbox] revision [toolbox-00001-l9c] has been deployed and is serving 100 percent of traffic.
Service URL: https://toolbox-868691532292.us-central1.run.app

student@instance-1:~$

Verify The Service

Now we can check if the service is up and we can access the endpoint. We use gcloud utility to get the retrieval service endpoint and the authentication token. Alternatively you can check the service URI in the cloud console.

You can copy the value and replace in the curl command the "$(gcloud run services list –filter="(toolbox)" –format="value(URL)" part .

Here is how to get the URL dynamically from the command line:

curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $(gcloud  run services list --filter="(toolbox)" --format="value(URL)")

Expected console output:

student@instance-1:~$ curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $(gcloud  run services list --filter="(toolbox)" --format="value(URL)")
🧰 Hello, World! 🧰student@instance-1:~$

If we see the "Hello World" message it means our service is up and serving the requests.

8. Deploy Sample Application

Now when we have the retrieval service up and running we can deploy a sample application. The application represents an online airport assistant which can give you information about flights, airports and even book a flight based on the flights and airport data from our database.

The application can be deployed locally, on a VM in the cloud or any other service like Cloud Run or Kubernetes. Here we are going to show how to deploy it on the VM first.

Prepare the environment

We continue to work on our VM using the same SSH session. To run our application we need some Python modules and we have already added them when we initiated our database earlier. Let's switch to our Python virtual environment and change our location to the app directory.

In the VM SSH session execute:

source ~/.venv/bin/activate
cd cymbal-air-toolbox-demo

Expected output (redacted):

student@instance-1:~$ source ~/.venv/bin/activate
cd cymbal-air-toolbox-demo
(.venv) student@instance-1:~/cymbal-air-toolbox-demo$

Run Assistant Application

Before starting the application we need to set up some environment variables. The basic functionality of the application such as query flights and airport amenities requires only TOOLBOX_URL which points application to the retrieval service. We can get it using the gcloud command .

In the VM SSH session execute:

export TOOLBOX_URL=$(gcloud  run services list --filter="(toolbox)" --format="value(URL)")

Expected output (redacted):

student@instance-1:~/cymbal-air-toolbox-demo$ export BASE_URL=$(gcloud  run services list --filter="(toolbox)" --format="value(URL)")

To use more advanced capabilities of the application like booking and changing flights we need to sign-in to the application using our Google account and for that purpose we need to provide CLIENT_ID environment variable using the OAuth client ID from the Prepare Client ID chapter:

export CLIENT_ID=215....apps.googleusercontent.com

Expected output (redacted):

student@instance-1:~/cymbal-air-toolbox-demo$ export CLIENT_ID=215....apps.googleusercontent.com

And now we can run our application:

python run_app.py

Expected output:

student@instance-1:~/cymbal-air-toolbox-demo/llm_demo$ python run_app.py
INFO:     Started server process [2900]
INFO:     Waiting for application startup.
Loading application...
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8081 (Press CTRL+C to quit)

Connect to the Application

You have several ways to connect to the application running on the VM. For example you can open port 8081 on the VM using firewall rules in the VPC or create a load balancer with public IP. Here we are going to use a SSH tunnel to the VM translating the local port 8080 to the VM port 8081.

Connecting From Local Machine

When we want to connect from a local machine we need to run a SSH tunnel. It can be done using gcloud compute ssh:

gcloud compute ssh instance-1 --zone=us-central1-a -- -L 8081:localhost:8081

Expected output:

student-macbookpro:~ student$ gcloud compute ssh instance-1 --zone=us-central1-a -- -L 8080:localhost:8081
Warning: Permanently added 'compute.7064281075337367021' (ED25519) to the list of known hosts.
Linux instance-1.us-central1-c.c.gleb-test-001.internal 6.1.0-21-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.90-1 (2024-05-03) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
student@instance-1:~$

Now we can open the browser and use http://localhost:8081 to connect to our application. We should see the application screen.

Connecting From Cloud Shell

Alternatively we can use Google Cloud Shell to connect. Open another Cloud Shell tab using the sign "+" at the top.

In the new tab get the origin and redirect URI for your web client executing the gcloud command:

echo "origin:"; echo "https://8080-$WEB_HOST"; echo "redirect:"; echo "https://8080-$WEB_HOST/login/google"

Here is the expected output:

student@cloudshell:~ echo "origin:"; echo "https://8080-$WEB_HOST"; echo "redirect:"; echo "https://8080-$WEB_HOST/login/google"
origin:
https://8080-cs-35704030349-default.cs-us-east1-rtep.cloudshell.dev
redirect:
https://8080-cs-35704030349-default.cs-us-east1-rtep.cloudshell.dev/login/google

And use the origin and the redirect of URIs as the "Authorized JavaScript origins" and "Authorized redirect URIs" for our credentials created in the "Prepare Client ID" chapter replacing or adding to the originally provided http://localhost:8080 values.

Click on "Cymbal Air" on the OAuth 2.0 client IDs page.

Put the origin and redirect URIs for the Cloud Shell and push the Save button.

In the new cloud shell tab start the tunnel to your VM by executing the gcloud command:

gcloud compute ssh instance-1 --zone=us-central1-a -- -L 8080:localhost:8081

If it will show an error "Cannot assign requested address" - please ignore it.

Here is the expected output:

student@cloudshell:~ gcloud compute ssh instance-1 --zone=us-central1-a -- -L 8080:localhost:8081
bind [::1]:8081: Cannot assign requested address
inux instance-1.us-central1-a.c.gleb-codelive-01.internal 6.1.0-21-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.90-1 (2024-05-03) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sat May 25 19:15:46 2024 from 35.243.235.73
student@instance-1:~$

It opens port 8080 on your cloud shell which can be used for the "Web preview".

Click on the "Web preview" button on the right top of your Cloud Shell and from the drop down menu choose "Preview on port 8080"

It opens a new tab in your web browser with the application interface. You should be able to see the "Cymbal Air Customer Service Assistant" page.

Sign into the Application

When everything is set up and your application is open we can use the "Sign in" button at the top right of our application screen to provide our credentials. That is optional and required only if you want to try booking functionality of the application.

It will open a pop-up window where we can choose our credentials.

After signing in the application is ready and you can start to post your requests into the field at the bottom of the window.

This demo showcases the Cymbal Air customer service assistant. Cymbal Air is a fictional passenger airline. The assistant is an AI chatbot that helps travelers to manage flights and look up information about Cymbal Air's hub at San Francisco International Airport (SFO).

Without signing in (without CLIENT_ID) it can help answer users questions like:

When is the next flight to Denver?

Are there any luxury shops around gate C28?

Where can I get coffee near gate A6?

Where can I buy a gift?

Please find a flight from SFO to Denver departing today

When you are signed in to the application you can try other capabilities like booking flights or check if the seat assigned to you is a window or aisle seat.

The application uses the latest Google foundation models to generate responses and augment it by information about flights and amenities from the operational AlloyDB database. You can read more about this demo application on the Github page of the project.

9. Clean up environment

Now when all tasks are completed we can clean up our environment

Delete Cloud Run Service

In Cloud Shell execute:

gcloud run services delete toolbox --region us-central1

Expected console output:

student@cloudshell:~ (gleb-test-short-004)$ gcloud run services delete retrieval-service --region us-central1
Service [retrieval-service] will be deleted.

Do you want to continue (Y/n)?  Y

Deleting [retrieval-service]...done.                                                                                                                                                                                                                 
Deleted service [retrieval-service].

Delete the Service Account for cloud run service

In Cloud Shell execute:

PROJECT_ID=$(gcloud config get-value project)
gcloud iam service-accounts delete toolbox-identity@$PROJECT_ID.iam.gserviceaccount.com --quiet

Expected console output:

student@cloudshell:~ (gleb-test-short-004)$ PROJECT_ID=$(gcloud config get-value project)
Your active configuration is: [cloudshell-222]
student@cloudshell:~ (gleb-test-short-004)$ gcloud iam service-accounts delete retrieval-identity@$PROJECT_ID.iam.gserviceaccount.com --quiet
deleted service account [[email protected]]
student@cloudshell:~ (gleb-test-short-004)$

Destroy the AlloyDB instances and cluster when you are done with the lab.

Delete AlloyDB cluster and all instances

If you've used the trial version of AlloyDB. Do not delete the trial cluster if you have plans to test other labs and resources using the trial cluster. You will not be able to create another trial cluster in the same project.

The cluster is destroyed with option force which also deletes all the instances belonging to the cluster.

In the cloud shell define the project and environment variables if you've been disconnected and all the previous settings are lost:

gcloud config set project <your project id>
export REGION=us-central1
export ADBCLUSTER=alloydb-aip-01
export PROJECT_ID=$(gcloud config get-value project)

Delete the cluster:

gcloud alloydb clusters delete $ADBCLUSTER --region=$REGION --force

📝 Note: The command takes 3-5 minutes to execute

Expected console output:

student@cloudshell:~ (test-project-001-402417)$ gcloud alloydb clusters delete $ADBCLUSTER --region=$REGION --force

All of the cluster data will be lost when the cluster is deleted.

Do you want to continue (Y/n)?  Y

Operation ID: operation-1697820178429-6082890a0b570-4a72f7e4-4c5df36f
Deleting cluster...done.   

Delete AlloyDB Backups

Delete all AlloyDB backups for the cluster:

📝 Note: The command will destroy all data backups for the cluster with name specified in environment variable

for i in $(gcloud alloydb backups list --filter="CLUSTER_NAME: projects/$PROJECT_ID/locations/$REGION/clusters/$ADBCLUSTER" --format="value(name)" --sort-by=~createTime) ; do gcloud alloydb backups delete $(basename $i) --region $REGION --quiet; done

Expected console output:

student@cloudshell:~ (test-project-001-402417)$ for i in $(gcloud alloydb backups list --filter="CLUSTER_NAME: projects/$PROJECT_ID/locations/$REGION/clusters/$ADBCLUSTER" --format="value(name)" --sort-by=~createTime) ; do gcloud alloydb backups delete $(basename $i) --region $REGION --quiet; done
Operation ID: operation-1697826266108-60829fb7b5258-7f99dc0b-99f3c35f
Deleting backup...done.    

Now we can destroy our VM

Delete GCE VM

In Cloud Shell execute:

export GCEVM=instance-1
export ZONE=us-central1-a
gcloud compute instances delete $GCEVM \
    --zone=$ZONE \
    --quiet

Expected console output:

student@cloudshell:~ (test-project-001-402417)$ export GCEVM=instance-1
export ZONE=us-central1-a
gcloud compute instances delete $GCEVM \
    --zone=$ZONE \
    --quiet
Deleted 

Delete the Service Account for GCE VM and The Retrieval service

In Cloud Shell execute:

PROJECT_ID=$(gcloud config get-value project)
gcloud iam service-accounts delete compute-aip@$PROJECT_ID.iam.gserviceaccount.com --quiet

Expected console output:

student@cloudshell:~ (gleb-test-short-004)$ PROJECT_ID=$(gcloud config get-value project)
gcloud iam service-accounts delete compute-aip@$PROJECT_ID.iam.gserviceaccount.com --quiet
Your active configuration is: [cloudshell-222]
deleted service account [[email protected]]
student@cloudshell:~ (gleb-test-short-004)$ 

10. Congratulations

Congratulations for completing the codelab.

What we've covered

✅ How to deploy AlloyDB Cluster
✅ How to connect to the AlloyDB
✅ How to configure and deploy MCP Toolbox Service
✅ How to deploy a sample application using the deployed service

AlloyDB Agentic RAG Application with MCP Toolbox [Part 1]

2025-11-26 05:17:28

1. Introduction

In this codelab, you will learn how to create an AlloyDB cluster, deploy the MCP toolbox, and configure it to use AlloyDB as a data source. You'll then build a sample interactive RAG application that uses the deployed toolbox to ground its requests.

You can get more information about the MCP Toolbox on the documentation page and the sample Cymbal Air application here.

This lab is part of a lab collection dedicated to AlloyDB AI features. You can read more on the AlloyDB AI page in documentation and see other labs.

Prerequisites

  • A basic understanding of the Google Cloud Console
  • Basic skills in command line interface and Google Cloud shell

What you'll learn

✅ How to deploy AlloyDB Cluster with Vertex AI integration
✅ How to connect to the AlloyDB
✅ How to configure and deploy MCP Tooolbox Service
✅ How to deploy a sample application using the deployed service

What you'll need

  • A Google Cloud Account and Google Cloud Project
  • A web browser such as Chrome

2. Setup and Requirements

Self-paced environment setup

  1. Sign-in to the Google Cloud Console and create a new project or reuse an existing one. If you don't already have a Gmail or Google Workspace account, you must create one.
  • The Project name is the display name for this project's participants. It is a character string not used by Google APIs. You can always update it.
  • The Project ID is unique across all Google Cloud projects and is immutable (cannot be changed after it has been set). The Cloud Console auto-generates a unique string; usually you don't care what it is. In most codelabs, you'll need to reference your Project ID (typically identified as PROJECT_ID). If you don't like the generated ID, you might generate another random one. Alternatively, you can try your own, and see if it's available. It can't be changed after this step and remains for the duration of the project.
  • For your information, there is a third value, a Project Number, which some APIs use. Learn more about all three of these values in the documentation.

⚠️ Caution: A project ID is globally unique and can't be used by anyone else after you've selected it. You are the only user of that ID. Even if a project is deleted, the ID can't be used again

📝 Note: If you use a Gmail account, you can leave the default location set to No organization. If you use a Google Workspace

  1. Next, you'll need to enable billing in the Cloud Console to use Cloud resources/APIs. Running through this codelab won't cost much, if anything at all. To shut down resources to avoid incurring billing beyond this tutorial, you can delete the resources you created or delete the project. New Google Cloud users are eligible for the $300 USD Free Trial program.

Start Cloud Shell

While Google Cloud can be operated remotely from your laptop, in this codelab you will be using Google Cloud Shell, a command line environment running in the Cloud.

From the Google Cloud Console, click the Cloud Shell icon on the top right toolbar:

Activate the Cloud Shell

It should only take a few moments to provision and connect to the environment. When it is finished, you should see something like this:

Screenshot of Google Cloud Shell terminal showing that the environment has connected

This virtual machine is loaded with all the development tools you'll need. It offers a persistent 5GB home directory, and runs on Google Cloud, greatly enhancing network performance and authentication. All of your work in this codelab can be done within a browser. You do not need to install anything.

3. Before you begin

Enable API

Output:

Please be aware that some resources you enable are going to incur some cost if you are not using the promotional tier. In normal circumstances if all the resources are destroyed upon completion of the lab the cost of all resources would not exceed $5. We recommend checking your billing and making sure the exercise is acceptable for you.

Inside Cloud Shell, make sure that your project ID is setup:

Usually the project ID is shown in parentheses in the command prompt in the cloud shell as it is shown in the picture:


gcloud config set project [YOUR-PROJECT-ID]

Then set the PROJECT_ID environment variable to your Google Cloud project ID:

PROJECT_ID=$(gcloud config get-value project)

Enable all necessary services:

gcloud services enable alloydb.googleapis.com \
                       compute.googleapis.com \
                       cloudresourcemanager.googleapis.com \
                       servicenetworking.googleapis.com \
                       vpcaccess.googleapis.com \
                       aiplatform.googleapis.com \
                       cloudbuild.googleapis.com \
                       artifactregistry.googleapis.com \
                       run.googleapis.com \
                       iam.googleapis.com \
                       secretmanager.googleapis.com

Expected output

student@cloudshell:~ (gleb-test-short-004)$ gcloud services enable alloydb.googleapis.com \
                       compute.googleapis.com \
                       cloudresourcemanager.googleapis.com \
                       servicenetworking.googleapis.com \
                       vpcaccess.googleapis.com \
                       aiplatform.googleapis.com \
                       cloudbuild.googleapis.com \
                       artifactregistry.googleapis.com \
                       run.googleapis.com \
                       iam.googleapis.com \
                       secretmanager.googleapis.com
Operation "operations/acf.p2-404051529011-664c71ad-cb2b-4ab4-86c1-1f3157d70ba1" finished successfully.

4. Deploy AlloyDB Cluster

Create AlloyDB cluster and primary instance. The following procedure describes how to create an AlloyDB cluster and instance using Google Cloud SDK. If you prefer the console approach you can follow the documentation here.

Before creating an AlloyDB cluster we need an available private IP range in our VPC to be used by the future AlloyDB instance. If we don't have it then we need to create it, assign it to be used by internal Google services and after that we will be able to create the cluster and instance.

Create private IP range

📝 Note: This step is required only if you don't already have an unused private IP range assigned to work with Google internal services.

We need to configure Private Service Access configuration in our VPC for AlloyDB. The assumption here is that we have the "default" VPC network in the project and it is going to be used for all actions.

Create the private IP range:

gcloud compute addresses create psa-range \
    --global \
    --purpose=VPC_PEERING \
    --prefix-length=24 \
    --description="VPC private service access" \
    --network=default

Create private connection using the allocated IP range:

gcloud services vpc-peerings connect \
    --service=servicenetworking.googleapis.com \
    --ranges=psa-range \
    --network=default

📝 Note: The second command takes a couple of minutes to execute

Expected console output:

student@cloudshell:~ (test-project-402417)$ gcloud compute addresses create psa-range \
    --global \
    --purpose=VPC_PEERING \
    --prefix-length=24 \
    --description="VPC private service access" \
    --network=default
Created [https://www.googleapis.com/compute/v1/projects/test-project-402417/global/addresses/psa-range].

student@cloudshell:~ (test-project-402417)$ gcloud services vpc-peerings connect \
    --service=servicenetworking.googleapis.com \
    --ranges=psa-range \
    --network=default
Operation "operations/pssn.p24-4470404856-595e209f-19b7-4669-8a71-cbd45de8ba66" finished successfully.

student@cloudshell:~ (test-project-402417)$

Create AlloyDB Cluster

In this section we are creating an AlloyDB cluster in the us-central1 region.

Define password for the postgres user. You can define your own password or use a random function to generate one

export PGPASSWORD=`openssl rand -hex 12`

Expected console output:

student@cloudshell:~ (test-project-402417)$ export PGPASSWORD=`openssl rand -hex 12`

Note the PostgreSQL password for future use.

echo $PGPASSWORD

You will need that password in the future to connect to the instance as the postgres user. I suggest writing it down or copying it somewhere to be able to use later.

Expected console output:

student@cloudshell:~ (test-project-402417)$ echo $PGPASSWORD
bbefbfde7601985b0dee5723

Create a Free Trial Cluster

If you haven't been using AlloyDB before you can create a free trial cluster:

Define region and AlloyDB cluster name. We are going to use us-central1 region and alloydb-aip-01 as a cluster name:

export REGION=us-central1
export ADBCLUSTER=alloydb-aip-01

Run command to create the cluster:

gcloud alloydb clusters create $ADBCLUSTER \
    --password=$PGPASSWORD \
    --network=default \
    --region=$REGION \
    --subscription-type=TRIAL

Expected console output:

export REGION=us-central1
export ADBCLUSTER=alloydb-aip-01
gcloud alloydb clusters create $ADBCLUSTER \
    --password=$PGPASSWORD \
    --network=default \
    --region=$REGION \
    --subscription-type=TRIAL
Operation ID: operation-1697655441138-6080235852277-9e7f04f5-2012fce4
Creating cluster...done.

Create an AlloyDB primary instance for our cluster in the same cloud shell session. If you are disconnected you will need to define the region and cluster name environment variables again.

📝 Note: The instance creation usually takes 6-10 minutes to complete

gcloud alloydb instances create $ADBCLUSTER-pr \
    --instance-type=PRIMARY \
    --cpu-count=8 \
    --region=$REGION \
    --cluster=$ADBCLUSTER

Expected console output:

student@cloudshell:~ (test-project-402417)$ gcloud alloydb instances create $ADBCLUSTER-pr \
    --instance-type=PRIMARY \
    --cpu-count=8 \
    --region=$REGION \
    --availability-type ZONAL \
    --cluster=$ADBCLUSTER
Operation ID: operation-1697659203545-6080315c6e8ee-391805db-25852721
Creating instance...done.

Create AlloyDB Standard Cluster

If it is not your first AlloyDB cluster in the project proceed with creation of a standard cluster.

Define region and AlloyDB cluster name. We are going to use us-central1 region and alloydb-aip-01 as a cluster name:

export REGION=us-central1
export ADBCLUSTER=alloydb-aip-01

Run command to create the cluster:

gcloud alloydb clusters create $ADBCLUSTER \
    --password=$PGPASSWORD \
    --network=default \
    --region=$REGION

Expected console output:

export REGION=us-central1
export ADBCLUSTER=alloydb-aip-01
gcloud alloydb clusters create $ADBCLUSTER \
    --password=$PGPASSWORD \
    --network=default \
    --region=$REGION 
Operation ID: operation-1697655441138-6080235852277-9e7f04f5-2012fce4
Creating cluster...done.   

Create an AlloyDB primary instance for our cluster in the same cloud shell session. If you are disconnected you will need to define the region and cluster name environment variables again.

📝 Note: The instance creation usually takes 6-10 minutes to complete

gcloud alloydb instances create $ADBCLUSTER-pr \
    --instance-type=PRIMARY \
    --cpu-count=2 \
    --region=$REGION \
    --cluster=$ADBCLUSTER

Expected console output:

student@cloudshell:~ (test-project-402417)$ gcloud alloydb instances create $ADBCLUSTER-pr \
    --instance-type=PRIMARY \
    --cpu-count=2 \
    --region=$REGION \
    --availability-type ZONAL \
    --cluster=$ADBCLUSTER
Operation ID: operation-1697659203545-6080315c6e8ee-391805db-25852721
Creating instance...done.  

Grant Necessary Permissions to AlloyDB

Add Vertex AI permissions to the AlloyDB service agent.

Open another Cloud Shell tab using the sign "+" at the top.

In the new cloud shell tab execute:

PROJECT_ID=$(gcloud config get-value project)
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:service-$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")@gcp-sa-alloydb.iam.gserviceaccount.com" \
  --role="roles/aiplatform.user"

Expected console output:

student@cloudshell:~ (test-project-001-402417)$ PROJECT_ID=$(gcloud config get-value project)
Your active configuration is: [cloudshell-11039]
student@cloudshell:~ (test-project-001-402417)$ gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:service-$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")@gcp-sa-alloydb.iam.gserviceaccount.com" \
  --role="roles/aiplatform.user"
Updated IAM policy for project [test-project-001-402417].
bindings:
- members:
  - serviceAccount:[email protected]
  role: roles/aiplatform.user
- members:
...
etag: BwYIEbe_Z3U=
version: 1

Close the tab by either execution command "exit" in the tab:

exit

5. Prepare GCE Virtual Machine

We are going to use a Google Compute Engine (GCE) VM as our platform to work with the database and deploy different parts of the sample application. Using a VM gives us more flexibility in installed components and direct access to the private AlloyDB IP for data preparation steps.

Create Service Account

Since we will use our VM to deploy the MCP Toolbox as a service and deploy or host the sample application, the first step is to create a Google Service Account (GSA). The GSA will be used by the GCE VM, and we will need to grant it the necessary privileges to work with other services.

In the Cloud Shell execute:

PROJECT_ID=$(gcloud config get-value project)
gcloud iam service-accounts create compute-aip --project $PROJECT_ID

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:compute-aip@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/cloudbuild.builds.editor"

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:compute-aip@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/artifactregistry.admin"

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:compute-aip@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/storage.admin"

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:compute-aip@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/run.admin"

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:compute-aip@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/iam.serviceAccountUser"

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:compute-aip@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/alloydb.viewer"

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:compute-aip@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/alloydb.client"

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:compute-aip@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/aiplatform.user"

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:compute-aip@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/serviceusage.serviceUsageConsumer"

gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member serviceAccount:compute-aip@$PROJECT_ID.iam.gserviceaccount.com \
    --role roles/secretmanager.admin
Deploy GCE VM
Create a GCE VM in the same region and VPC as the AlloyDB cluster.

In Cloud Shell execute:

ZONE=us-central1-a
PROJECT_ID=$(gcloud config get-value project)
gcloud compute instances create instance-1 \
    --zone=$ZONE \
    --create-disk=auto-delete=yes,boot=yes,image=projects/debian-cloud/global/images/$(gcloud compute images list --filter="family=debian-12 AND family!=debian-12-arm64" --format="value(name)") \
    --scopes=https://www.googleapis.com/auth/cloud-platform \
    --service-account=compute-aip@$PROJECT_ID.iam.gserviceaccount.com

Expected console output:

student@cloudshell:~ (test-project-402417)$ ZONE=us-central1-a
PROJECT_ID=$(gcloud config get-value project)
gcloud compute instances create instance-1 \
    --zone=$ZONE \
    --create-disk=auto-delete=yes,boot=yes,image=projects/debian-cloud/global/images/$(gcloud compute images list --filter="family=debian-12 AND family!=debian-12-arm64" --format="value(name)") \
    --scopes=https://www.googleapis.com/auth/cloud-platform \
    --service-account=compute-aip@$PROJECT_ID.iam.gserviceaccount.com
Your active configuration is: [cloudshell-10282]
Created [https://www.googleapis.com/compute/v1/projects/gleb-test-short-002-470613/zones/us-central1-a/instances/instance-1].
NAME: instance-1
ZONE: us-central1-a
MACHINE_TYPE: n1-standard-1
PREEMPTIBLE: 
INTERNAL_IP: 10.128.0.2
EXTERNAL_IP: 34.28.55.32
STATUS: RUNNING

Install Postgres Client

Install the PostgreSQL client software on the deployed VM

Connect to the VM:

🗒️ Note: First time the SSH connection to the VM can take longer since the process includes creation of RSA key for secure connection and propagating the public part of the key to the project

gcloud compute ssh instance-1 --zone=us-central1-a

Expected console output:

student@cloudshell:~ (test-project-402417)$ gcloud compute ssh instance-1 --zone=us-central1-a
Updating project ssh metadata...working..Updated [https://www.googleapis.com/compute/v1/projects/test-project-402417].                                                                                                                                                         
Updating project ssh metadata...done.                                                                                                                                                                                                                                              
Waiting for SSH key to propagate.
Warning: Permanently added 'compute.5110295539541121102' (ECDSA) to the list of known hosts.
Linux instance-1 5.10.0-26-cloud-amd64 #1 SMP Debian 5.10.197-1 (2023-09-29) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
student@instance-1:~$ 

Install the software running command inside the VM:

sudo apt-get update
sudo apt-get install --yes postgresql-client

Expected console output:

student@instance-1:~$ sudo apt-get update
sudo apt-get install --yes postgresql-client
Get:1 file:/etc/apt/mirrors/debian.list Mirrorlist [30 B]
Get:4 file:/etc/apt/mirrors/debian-security.list Mirrorlist [39 B]
Hit:7 https://packages.cloud.google.com/apt google-compute-engine-bookworm-stable InRelease
Get:8 https://packages.cloud.google.com/apt cloud-sdk-bookworm InRelease [1652 B]
Get:2 https://deb.debian.org/debian bookworm InRelease [151 kB]
Get:3 https://deb.debian.org/debian bookworm-updates InRelease [55.4 kB]
...redacted...
update-alternatives: using /usr/share/postgresql/15/man/man1/psql.1.gz to provide /usr/share/man/man1/psql.1.gz (psql.1.gz) in auto mode
Setting up postgresql-client (15+248) ...
Processing triggers for man-db (2.11.2-2) ...
Processing triggers for libc-bin (2.36-9+deb12u7) ...

Connect to the AlloyDB Instance

Connect to the primary instance from the VM using psql.

Continue with the opened SSH session to your VM. If you have been disconnected then connect again using the same command as above.

Use the previously noted $PGASSWORD and the cluster name to connect to AlloyDB from the GCE VM:

export PGPASSWORD=<Noted password>
PROJECT_ID=$(gcloud config get-value project)
REGION=us-central1
ADBCLUSTER=alloydb-aip-01
INSTANCE_IP=$(gcloud alloydb instances describe $ADBCLUSTER-pr --cluster=$ADBCLUSTER --region=$REGION --format="value(ipAddress)")
psql "host=$INSTANCE_IP user=postgres sslmode=require"

Expected console output:

student@instance-1:~$ PROJECT_ID=$(gcloud config get-value project)
REGION=us-central1
ADBCLUSTER=alloydb-aip-01
INSTANCE_IP=$(gcloud alloydb instances describe $ADBCLUSTER-pr --cluster=$ADBCLUSTER --region=$REGION --format="value(ipAddress)")
psql "host=$INSTANCE_IP user=postgres sslmode=require"
psql (15.13 (Debian 15.13-0+deb12u1), server 16.8)
WARNING: psql major version 15, server major version 16.
         Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

postgres=>

Exit from the psql session keeping the SSH connection up:

exit

Expected console output:

postgres=> exit
student@instance-1:~$ 

6. Initialize the database

We are going to use our client VM as a platform to populate our database with data and host our application. The first step is to create a database and populate it with data.

Create Database

Create a database with the name "assistantdemo".

In the GCE VM session execute:

📝 Note: If your SSH session was terminated you need to reset your environment variables such as:

export PGPASSWORD=

export REGION=us-central1

export ADBCLUSTER=alloydb-aip-01

export INSTANCE_IP=$(gcloud alloydb instances describe $ADBCLUSTER-pr --cluster=$ADBCLUSTER --region=$REGION --format="value(ipAddress)")

psql "host=$INSTANCE_IP user=postgres" -c "CREATE DATABASE assistantdemo"  

Expected console output:

student@instance-1:~$ psql "host=$INSTANCE_IP user=postgres" -c "CREATE DATABASE assistantdemo"
CREATE DATABASE
student@instance-1:~$  

Prepare Python Environment

To continue we are going to use prepared Python scripts from GitHub repository but before doing that we need to install the required software.

In the GCE VM execute:

sudo apt install -y python3.11-venv git
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip

Expected console output:

student@instance-1:~$ sudo apt install -y python3.11-venv git
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  git-man liberror-perl patch python3-distutils python3-lib2to3 python3-pip-whl python3-setuptools-whl
Suggested packages:
  git-daemon-run | git-daemon-sysvinit git-doc git-email git-gui gitk gitweb git-cvs git-mediawiki git-svn ed diffutils-doc
The following NEW packages will be installed:
  git git-man liberror-perl patch python3-distutils python3-lib2to3 python3-pip-whl python3-setuptools-whl python3.11-venv
0 upgraded, 9 newly installed, 0 to remove and 2 not upgraded.
Need to get 12.4 MB of archives.
After this operation, 52.2 MB of additional disk space will be used.
Get:1 file:/etc/apt/mirrors/debian.list Mirrorlist [30 B]
...redacted...
Installing collected packages: pip
  Attempting uninstall: pip
    Found existing installation: pip 23.0.1
    Uninstalling pip-23.0.1:
      Successfully uninstalled pip-23.0.1
Successfully installed pip-24.0
(.venv) student@instance-1:~$

Verify Python version.

In the GCE VM execute:

python -V

Expected console output:

(.venv) student@instance-1:~$ python -V
Python 3.11.2
(.venv) student@instance-1:~$ 

Install MCP Toolbox Locally

MCP Toolbox for Databases (later in the text MCP toolbox or toolbox) is an open source MCP server working with different data sources. It helps you to develop tools faster by providing a level of abstraction for different data sources and adding features like authentication and connection pooling. You can read about all the features on the official page.

We are going to use the MCP toolbox to initiate our sample dataset and later to be used as MCP server to handle data source requests from our application during Retrieval Augmented Generation (RAG) flow.

Let's install the MCP toolbox locally to populate the assistantdemo database.

In the GCE VM execute:

export VERSION=0.16.0
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
chmod +x toolbox

Expected console output:

(.venv) student@instance-1:~$ export VERSION=0.16.0
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
chmod +x toolbox
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  133M  100  133M    0     0   158M      0 --:--:-- --:--:-- --:--:--  158M

Run Toolbox for Data Initialization

In the GCE VM execute:

📝Note: If your SSH session was terminated by inactivity or any other reason you need to set your environment variables such as:

export PGPASSWORD=

REGION=us-central1

ADBCLUSTER=alloydb-aip-01

INSTANCE_IP=$(gcloud alloydb instances describe $ADBCLUSTER-pr --cluster=$ADBCLUSTER --region=$REGION --format="value(ipAddress)")

Export environment variables for database population:

export ALLOYDB_POSTGRES_PROJECT=$(gcloud config get-value project)
export ALLOYDB_POSTGRES_REGION="us-central1"
export ALLOYDB_POSTGRES_CLUSTER="alloydb-aip-01"
export ALLOYDB_POSTGRES_INSTANCE="alloydb-aip-01-pr"
export ALLOYDB_POSTGRES_DATABASE="assistantdemo"
export ALLOYDB_POSTGRES_USER="postgres"
export ALLOYDB_POSTGRES_PASSWORD=$PGPASSWORD
export ALLOYDB_POSTGRES_IP_TYPE="private"

Start toolbox for the database initiation. It will start the process locally which will help you to connect seamlessly to the destination database on AlloyDB to fill it up with sample data.

./toolbox --prebuilt alloydb-postgres

Expected console output. You should see in the last line of the output - "Server ready to serve!":

student@instance-1:~$ cexport ALLOYDB_POSTGRES_PROJECT=$PROJECT_ID
export ALLOYDB_POSTGRES_REGION="us-central1"
export ALLOYDB_POSTGRES_CLUSTER="alloydb-aip-01"
export ALLOYDB_POSTGRES_INSTANCE="alloydb-aip-01-pr"
export ALLOYDB_POSTGRES_DATABASE="assistantdemo"
export ALLOYDB_POSTGRES_USER="postgres"
export ALLOYDB_POSTGRES_PASSWORD=$PGPASSWORD
export ALLOYDB_POSTGRES_IP_TYPE="private"
student@instance-1:~$ ./toolbox --prebuilt alloydb-postgres
2025-09-02T18:30:58.957655886Z INFO "Using prebuilt tool configuration for alloydb-postgres" 
2025-09-02T18:30:59.507306664Z INFO "Initialized 1 sources." 
2025-09-02T18:30:59.50748379Z INFO "Initialized 0 authServices." 
2025-09-02T18:30:59.507618807Z INFO "Initialized 2 tools." 
2025-09-02T18:30:59.507726704Z INFO "Initialized 2 toolsets." 
2025-09-02T18:30:59.508258894Z INFO "Server ready to serve!" 

Do not exit or close this tab of the Cloud Shell until data population is complete.

Populate Database

Open another Cloud Shell tab using the sign "+" at the top.

And connect to the instance-1 VM:

gcloud compute ssh instance-1 --zone=us-central1-a

Expected console output:

student@cloudshell:~ (test-project-402417)$ gcloud compute ssh instance-1 --zone=us-central1-a
Linux instance-1 6.1.0-37-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.140-1 (2025-05-22) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Sep  2 21:44:07 2025 from 35.229.111.9
student@instance-1:~$ 

Clone the GitHub repository with the code for the retrieval service and sample application.

In the GCE VM execute:

git clone  https://github.com/GoogleCloudPlatform/cymbal-air-toolbox-demo.git

Expected console output:

student@instance-1:~$ git clone  https://github.com/GoogleCloudPlatform/cymbal-air-toolbox-demo.git
Cloning into 'cymbal-air-toolbox-demo'...
remote: Enumerating objects: 3481, done.
remote: Counting objects: 100% (47/47), done.
remote: Compressing objects: 100% (41/41), done.
remote: Total 3481 (delta 16), reused 7 (delta 5), pack-reused 3434 (from 3)
Receiving objects: 100% (3481/3481), 57.96 MiB | 6.04 MiB/s, done.
Resolving deltas: 100% (2549/2549), done.
student@instance-1:~

Please pay attention if you have any errors.

Prepare Python environment and install requirement packages:

source .venv/bin/activate
cd cymbal-air-toolbox-demo
pip install -r requirements.txt

Set Python path to the repository root folder and run script to populate the database with the sample dataset. The first command is adding a path to our Python modules to our environment and the second command is populating our database with the data.

export PYTHONPATH=$HOME/cymbal-air-toolbox-demo
python data/run_database_init.py

Expected console output(redacted). You should see "database init done" at the end:

student@instance-1:~$ source .venv/bin/activate
(.venv) student@instance-1:~$ 
(.venv) student@instance-1:~$ cd cymbal-air-toolbox-demo/
(.venv) student@instance-1:~/cymbal-air-toolbox-demo$ pip install -r requirements.txt
python run_database_init.py
Collecting fastapi==0.115.0 (from -r requirements.txt (line 1))
  Downloading fastapi-0.115.0-py3-none-any.whl.metadata (27 kB)
Collecting google-auth==2.40.3 (from -r requirements.txt (line 2))
  Downloading google_auth-2.40.3-py2.py3-none-any.whl.metadata (6.2 kB)
Collecting google-cloud-aiplatform==1.97.0 (from google-cloud-aiplatform[evaluation]==1.97.0->-r requirements.txt (line 3))
  Downloading google_cloud_aiplatform-1.97.0-py2.py3-none-any.whl.metadata (36 kB)
Collecting itsdangerous==2.2.0 (from -r requirements.txt (line 4))
  Downloading itsdangerous-2.2.0-py3-none-any.whl.metadata (1.9 kB)
Collecting jinja2==3.1.5 (from -r requirements.txt (line 5))
  Downloading jinja2-3.1.5-py3-none-any.whl.metadata (2.6 kB)
Collecting langchain-community==0.3.25 (from -r requirements.txt (line 6))
  Downloading langchain_community-0.3.25-py3-none-any.whl.metadata (2.9 kB)
Collecting langchain==0.3.25 (from -r requirements.txt (line 7))
...

(.venv) student@instance-1:~/cymbal-air-toolbox-demo$ 
(.venv) student@instance-1:~/cymbal-air-toolbox-demo$ export PYTHONPATH=$HOME/cymbal-air-toolbox-demo
python data/run_database_init.py
Airports table initialized
Amenities table initialized
Flights table initialized
Tickets table initialized
Policies table initialized
database init done.
(.venv) student@instance-1:~/cymbal-air-toolbox-demo$ 

You can close this tab now.

In the VM session execute:

exit

And in the Cloud Shell session press ctrl+d or execute :

exit

In the first tab with running MCP Toolbox press ctrl+c in to exit from the toolbox running session.

The database has been populated with sample data for the application.

You can verify it by connecting to the database and checking the number of rows in the airports table. You can use the psql utility as we've used before or AlloyDB Studio . here is how you can check it using psql

In the ssh session to instance-1 VM execute:

export PGPASSWORD=<Noted AlloyDB password>

REGION=us-central1
ADBCLUSTER=alloydb-aip-01
INSTANCE_IP=$(gcloud alloydb instances describe $ADBCLUSTER-pr --cluster=$ADBCLUSTER --region=$REGION --format="value(ipAddress)")
psql "host=$INSTANCE_IP user=postgres dbname=assistantdemo" -c "SELECT COUNT(*) FROM airports"  

Expected console output:

student@instance-1:~$ REGION=us-central1
ADBCLUSTER=alloydb-aip-01
INSTANCE_IP=$(gcloud alloydb instances describe $ADBCLUSTER-pr --cluster=$ADBCLUSTER --region=$REGION --format="value(ipAddress)")
psql "host=$INSTANCE_IP user=postgres dbname=assistantdemo" -c "SELECT COUNT(*) FROM airports"
 count 
-------
  7698
(1 row)

The database is ready and we can move on to MCP Toolbox deployment.

You've completed Part 1 of the AlloyDB Agentic RAG application tutorial, please continue to Part 2.

AWS Blu Age Modernization: My Journey Through All 3 Certification Levels

2025-11-26 05:10:21

AWS Blu Age Modernization: My Journey Through All 3 Certification Levels

What is AWS Blu Age?

AWS Blu Age is an automated mainframe modernization solution that transforms legacy COBOL applications into modern Java Spring Boot applications running on AWS. It's part of AWS Mainframe Modernization service and uses AI-powered refactoring to convert decades-old code into cloud-native applications.

Key Value: Instead of manually rewriting millions of lines of COBOL code (which takes years), Blu Age automates 85-95% of the transformation in weeks.

Why It Matters

The Problem: Organizations run critical business applications on mainframes but face:

  • High operational costs (licensing, hardware, specialized staff)
  • Scarce COBOL talent
  • Inability to innovate quickly
  • Difficulty integrating with modern systems

The Solution: Blu Age transforms:

  • COBOL → Java Spring Boot
  • JCL batch jobs → AWS Batch/Step Functions
  • CICS transactions → REST APIs
  • DB2/IMS → PostgreSQL/Aurora
  • Mainframe → AWS Cloud

Core Components

  1. Blu Insights: Assessment tool to analyze mainframe code and create transformation roadmap
  2. Refactoring Engine: Automated code transformation with customizable rules
  3. Blu Age Runtime: Modern Java runtime environment for refactored applications
  4. Blu Age Developer: IDE for post-transformation customization

My Certification Journey

Level 1: Foundations (Black Belt)

Focus: Understanding mainframe modernization fundamentals

What I Learned:

  • Mainframe basics (COBOL, JCL, CICS, DB2)
  • AWS Mainframe Modernization service architecture
  • Assessment methodology using Blu Insights
  • Transformation strategies (Rehost vs Replatform vs Refactor)
  • Business case development and ROI calculation

Key Takeaway: Proper assessment is critical. Blu Insights analyzes your codebase to identify complexity, dependencies, and transformation effort before you start.

Level 2: Advanced Refactoring

Focus: Hands-on transformation and implementation

What I Learned:

  • Deep dive into refactoring engine mechanics
  • Custom transformation rules and patterns
  • Database migration strategies (DB2 to PostgreSQL)
  • Batch processing transformation (JCL to AWS Batch)
  • Online transaction processing (CICS to Spring Boot REST APIs)
  • Testing and validation approaches

Hands-On: Worked with CardDemo sample application - a complete mainframe banking app with COBOL programs, CICS transactions, VSAM files, and DB2 databases. Transformed it end-to-end.

Key Takeaway: The refactoring engine is highly accurate, but you need to understand the patterns to customize transformations for complex business logic.

Level 3: Expert Delivery

Focus: Production-ready implementations and customer delivery

What I Learned:

  • End-to-end project delivery methodology
  • Production deployment strategies (ECS, EKS, EC2)
  • Performance optimization and tuning
  • Ad-hoc modifications and customizations
  • Customer POC execution
  • Go-live planning and cutover strategies

Advanced Topics:

  • Microservices decomposition
  • CI/CD pipeline setup
  • Monitoring and observability
  • Troubleshooting production issues
  • Leading customer workshops

Key Takeaway: Success isn't just about transformation - it's about delivering production-ready, performant applications with proper DevOps practices.

The Modernization Process

1. Assessment (Blu Insights)

  • Upload mainframe source code
  • Analyze application portfolio
  • Identify dependencies and complexity
  • Generate effort estimates
  • Create transformation roadmap

2. Refactoring

  • Configure transformation rules
  • Execute automated refactoring
  • Generate Java Spring Boot code
  • Transform data structures
  • Create AWS deployment artifacts

3. Testing

  • Automated test generation
  • Functional equivalence testing
  • Performance benchmarking
  • User acceptance testing

4. Deployment

  • Deploy to AWS (containerized or VM-based)
  • Configure monitoring (CloudWatch, X-Ray)
  • Set up CI/CD pipelines
  • Implement security controls

5. Optimization

  • Performance tuning
  • Cost optimization
  • Microservices decomposition
  • Cloud-native enhancements

Real-World Applications

Financial Services: Core banking systems, payment processing
Insurance: Policy administration, claims processing
Government: Tax systems, benefits administration
Retail: Inventory management, order processing

Certification Path

Prerequisites:

  • Basic mainframe knowledge (helpful but not required)
  • AWS fundamentals
  • Java basics (for Level 2+)

Steps:

  1. Join AWS Partner Network (APN)
  2. Complete AWS Mainframe Modernization training
  3. Pass Level 1 exam (foundations)
  4. Complete hands-on labs for Level 2
  5. Pass Level 2 exam (refactoring)
  6. Participate in customer POCs for Level 3
  7. Pass Level 3 exam (expert delivery)

Study Resources:

  • AWS Skill Builder courses
  • AWS Partner Central training
  • Blu Age documentation
  • Sample applications (CardDemo, GenApp)
  • Hands-on workshops

Key Lessons Learned

  1. Assessment First: Never skip the assessment phase. Understanding your codebase complexity saves time later.

  2. Start Small: Begin with non-critical applications to build confidence and refine your process.

  3. Trust the Automation: The refactoring engine is highly accurate (85-95%), but always validate outputs.

  4. Data Migration is Critical: Plan database migration early. It's often more complex than code transformation.

  5. DevOps from Day One: Set up CI/CD pipelines immediately to accelerate testing and deployment.

  6. Business Involvement: Keep business stakeholders engaged throughout the process for validation.

Common Challenges & Solutions

Challenge: Complex business logic in COBOL
Solution: Use custom transformation rules and pattern recognition

Challenge: Data migration complexity
Solution: Leverage AWS DMS alongside Blu Age for seamless migration

Challenge: Testing effort
Solution: Automate test generation and use equivalence testing

Challenge: Skills gap
Solution: Hybrid teams with mainframe + cloud expertise

Why Get Certified?

  • Career Growth: Mainframe modernization is a multi-billion dollar market
  • Unique Skillset: Combination of legacy and modern cloud skills is rare
  • Customer Demand: Enterprises are actively seeking certified professionals
  • Hands-On Experience: Certification provides practical, real-world skills
  • AWS Recognition: Official AWS partner certification

Conclusion

Completing all three levels of AWS Blu Age certification has been transformative. The technology is mature, proven, and capable of handling the most complex mainframe modernization challenges.

If you're a solutions architect, developer, or IT leader, AWS Blu Age opens doors to exciting modernization opportunities. The mainframe era isn't ending - it's evolving into cloud-native applications that preserve decades of business logic while enabling modern innovation.

Ready to start? Visit AWS Mainframe Modernization service page and begin your Level 1 certification journey.

About: AWS Blu Age certified professional (L1, L2, L3 Black Belt).

AWS #BluAge #MainframeModernization #CloudMigration #COBOL #LegacyModernization

✅ *Authentication &amp; Authorization Basics* 🔐🌐

2025-11-26 05:04:20

🔹 What is Authentication?

It’s the process of verifying who a user is.

🔹 What is Authorization?

It’s the process of verifying what a user is allowed to do after logging in.

✅ Step 1: Authentication – Common Methods

Username & Password – Basic login

OAuth – Login via Google, GitHub, etc.

JWT (JSON Web Token) – Popular for token-based auth

Session-Based – Stores session on server with session ID

✅ Step 2: How Login Works (JWT Example)

  1. User sends email & password to server
  2. Server verifies and sends back a JWT
  3. JWT is stored in browser (usually localStorage)
  4. On each request, client sends JWT in headers
  5. Server checks token before giving access

✅ Step 3: Authorization Types

Role-Based Access – Admin, Editor, User

Resource-Based – Only owners can edit their content

Route Protection – Block some pages unless logged in

✅ Step 4: Protecting Routes (Frontend Example)

if (!localStorage.getItem('token')) {
  window.location.href = '/login';
}

✅ Step 5: Backend Route Protection (Express.js)

function authMiddleware(req, res, next) {
  const token = req.headers.authorization;
if (!token) return res.status(401).send('Access Denied');
  // Verify token and decode user info
  next();
}

✅ Step 6: Common Tools & Libraries

bcrypt – Hash passwords

jsonwebtoken (JWT) – Create & verify tokens

passport.js – Auth middleware

OAuth Providers – Google, Facebook, GitHub

✅ Step 7: Best Practices

• Always hash passwords (never store plain text)

• Use HTTPS

• Set token expiry (e.g. 15 mins)

• Refresh tokens securely

• Don't expose sensitive data in JWT

💬 and like for more

I Treated My Team Like Customers and Became a Better Manager

2025-11-26 05:03:20

I used to be terrible at 1-on-1s.

Not because I didn't care. I cared deeply about my team. But every meeting felt like I was starting from scratch.

"How's that project going?" I'd ask.

"I finished that two weeks ago," they'd remind me.

Awkward silence.

Then one day, I had a realization that changed everything.

The Sales Team Didn't Have This Problem

Our sales team managed relationships with 50+ customers each. Somehow, they never forgot what was discussed in the last call. They never asked the same question twice. They always knew exactly where each customer was in their journey.

How?

They had a CRM.

Before every customer call, a sales rep would pull up the customer's profile in Salesforce:

  • Complete interaction history
  • All previous conversations
  • Tracked commitments and follow-ups
  • Context for the relationship
  • Next steps clearly defined

Meanwhile, I was managing 8 engineers with scattered notes across Notion, Slack, Jira, and my brain.

The sales team wasn't smarter than me. They just had better systems.

The Experiment: What If I Treated My Team Like Customers?

I decided to try something.

I created a Confluence page for each person on my team. Not a project page. Not a meeting notes dump. A profile.

Just like a CRM, but for people management.

Here's what each profile looked like:

# Sammy  - Senior Engineer

## Career Goals
- Wants to move toward tech lead role
- Interested in infrastructure/DevOps work
- Loves backend systems, less interested in frontend

## Recent Context
- Just shipped auth refactor (2 sprints ahead of schedule)
- Feeling a bit siloed from the DevOps team
- Mentioned wanting more architectural responsibility

## 1-on-1 History (Newest First)

### 2024-01-15 - 1-on-1
**Discussed:**
- Auth refactor completion (shipped early!)
- Interest in learning Kubernetes
- Wants exposure to infrastructure work

**Action Items:**
- [ ] Me: Intro Sammy to DevOps team lead (Mark)
- [ ] Me: Add her to #architecture Slack channel
- [x] Her: Write tech spec for caching layer proposal

**Notes:**
- Really proud of auth work
- Feeling ready for more ownership
- Mentioned she's never worked with K8s before

### 2024-01-08 - 1-on-1
**Discussed:**
- Sprint planning for auth refactor
- Career development conversation
- Mentoring junior devs

**Action Items:**
- [x] Her: Finish auth refactor by EOW
- [x] Me: Add her to architecture review meetings

**Notes:**
- Did great job mentoring Alex on code reviews
- Wants more architectural decision-making

## Personal Context
- Has two kids (flexible schedule appreciated)
- Previous background in security engineering
- Prefers async communication

Simple. Clean. All in one place.

The First Meeting After I Changed My System

Before my next 1-on-1 with Paul, I spent 5 minutes reading his profile.

Last time, he'd mentioned he was nervous about presenting the database migration plan to the architecture team. I'd completely forgotten about it.

But it was right there in his profile.

I started the meeting differently:

"Hey Paul, last time you mentioned you were nervous about the database migration presentation. How'd it go?"

He paused. Looked surprised.

"Wait... you remembered that?"

"Of course," I said.

He smiled. "It went really well actually. The team approved the plan. We're starting migration next sprint."

Then he opened up about something he'd never mentioned before: he was interested in moving into an SRE role eventually, but wasn't sure how to get there.

That one moment changed our relationship.

It wasn't that I suddenly became a better listener. I just had a system that helped me remember.

What Changed After 3 Months

1. No More "What Were We Talking About?" Moments

Before: 10 minutes of fumbling through notes before each meeting.

After: 5 minutes reviewing their profile. I walked in prepared.

2. I Actually Followed Through on Commitments

Before: "I'll introduce you to the DevOps team" → forget → feel guilty → they stop asking

After: Open commitments tracked per person. I couldn't forget. It was right there every time I opened their profile.

3. I Could See Patterns Over Time

After 8 weeks of 1-on-1s with Maria, I noticed a pattern in my notes:

  • Week 1: Mentioned feeling burned out from on-call
  • Week 3: Mentioned incident response was draining
  • Week 5: Mentioned struggling with work-life balance
  • Week 8: Mentioned thinking about taking a break

I would have missed this completely with scattered notes. But seeing 8 weeks of history in one place? The pattern was obvious.

I talked to her about reducing on-call load and moving her to a project with fewer production incidents. She stayed. She's now one of my strongest senior engineers.

I would have lost her without this system.

4. Career Development Actually Happened

Before: "Let's talk about your career goals" → vague conversation → nothing happens

After: Career goals written at the top of their profile. I saw them before every 1-on-1. I could connect their current work to their long-term goals.

"You mentioned wanting to move toward tech lead. This API redesign project would be a great opportunity to lead. Want to own it?"

5. My Team Noticed

Three months in, Sammy said something in our 1-on-1:

"I feel like you actually remember our conversations now. It feels like you care more."

I did care before. But now I had a system that let me show it.

The Simple Formula

Here's what I learned:

Scattered notes + Working memory = Forgotten commitments
Person-centric profiles + 5 min prep = Relationships at scale

Sales teams figured this out decades ago:

  • CRM = Customer Relationship Management

Engineering managers need the same thing:

  • PRM = People Relationship Management

The Tipping Point

This system works at any scale, but it becomes essential around 5-7 direct reports.

Below 5 reports: Your brain can mostly keep up. You'll forget things occasionally, but it's manageable.

Above 7 reports: Working memory breaks down. You need a system.

I hit the crisis point at 8 engineers. That's when I built this.

If you're managing 5+ people and feeling scattered, you're not bad at your job. You just don't have the right system yet.

How to Build This System (15 Minutes)

You don't need fancy tools. Here's how to start:

Step 1: Create One Page Per Person (5 min)

Use whatever you have:

  • Confluence
  • Notion
  • Google Docs
  • Markdown files in a Git repo
  • Literal paper notebook

One page. One person.

Step 2: Add Three Sections (5 min)

# [Name] - [Role]

## Career Goals
(What do they want long-term?)

## 1-on-1 History
(Chronological log, newest first)

## Open Commitments
(Yours and theirs)

That's it. Three sections.

Step 3: Log Your Next 1-on-1 (5 min)

After your next meeting, spend 5 minutes writing:

  • Date
  • What you discussed
  • What they're working on
  • Wins, challenges, concerns
  • Action items (clearly labeled: you vs. them)

Step 4: Review Before Each Meeting (5 min)

Before your next 1-on-1 with them:

  • Read their profile
  • Check what you talked about last time
  • Check what you committed to
  • Check what they committed to

Show up prepared.

The ROI

Time saved:

  • Before: 10 min scrambling before each meeting
  • After: 5 min focused review
  • Savings: 5 min per meeting = 40 min/week for 8 reports

Trust gained:

  • Remember commitments → follow through → trust
  • Remember personal context → they feel seen
  • Remember career goals → they feel supported

Retention impact:

  • Replacing a senior engineer costs 6-12 months of salary
  • I kept Maria because I saw the burnout pattern
  • I kept Sammy because I followed through on DevOps intro
  • Avoiding 1 departure = $100k-200k saved

What I Realized

Systems aren't the opposite of care.

Systems enable care at scale.

I care about my team. I always have. But caring without systems meant:

  • Forgotten commitments
  • Repeated questions
  • Lost context
  • Eroded trust

Caring with systems meant:

  • Showing up prepared
  • Following through
  • Building continuity
  • Earning trust

The Mindset Shift

Before: "I should be better at remembering things."

After: "I should build systems that remember for me."

Sales reps don't feel guilty about using Salesforce. They use it because it makes them better at their job.

Engineering managers shouldn't feel guilty about using systems. They should feel guilty about not using them.

What Happened Next

This system worked so well that other managers started asking about it.

"Can you share your Confluence template?"

"How do you organize your notes?"

"What's your system?"

Eventually, I realized: every engineering manager needs this.

So I turned it into a product. It's called Helmly.

It's a CRM for people managers. Person-centric profiles. Meeting history auto-loaded. Open commitments tracked per person. No scattered notes. No context switching.

If you're managing 5+ engineers and feeling the scramble, check it out at helmly.io.

I am looking for ~20 founding members (free lifetime access) to help shape the product. If this article resonated, I'd love to have you join.

Start Today

You don't need Helmly to start. You just need one page per person.

Pick your tool:

  • Confluence
  • Notion
  • Google Docs
  • Markdown in Git
  • Physical notebook

Create one profile. Log one 1-on-1. Review it before your next meeting.

Your team will notice immediately.

What system do you use for 1-on-1s? Drop a comment. I'd love to hear what's working (or not working) for other engineering managers.

Follow my build log on Twitter @HelmlyApp - I'm building Helmly in public and sharing what I learn about management systems, product development, and scaling relationships.

ASP.NET Core + Docker: Mastering Multi-Stage Builds for Web APIs

2025-11-26 05:02:39

ASP.NET Core + Docker: Mastering Multi‑Stage Builds for Web APIs

ASP.NET Core + Docker: Mastering Multi‑Stage Builds for Web APIs

“Visual Studio gave me this Dockerfile… but what is it actually doing?”

If you’ve ever right‑clicked “Add > Docker Support” in an ASP.NET project, you’ve probably seen a fairly complex multi‑stage Dockerfile appear in your repo.

It looks smart. It builds. It even runs in Debug.

But:

  • What does each stage really do?
  • What do you actually need installed to make it work?
  • Why is there a mysterious USER $APP_UID line?
  • How do you safely build and run the final image in your own environment?

This guide takes a real Dockerfile for an ASP.NET Core Web API and turns it into a clear mental model you can reuse in any .NET + Docker project.

TL;DR — What You’ll Learn

✅ How a multi‑stage Dockerfile for ASP.NET Core is structured (base → build → publish → final)

✅ What the USER $APP_UID line does and how to avoid permission problems

✅ What you actually need installed to build and run this image

✅ How to build and run the container step by step

✅ Why aligning aspnet:9.0 and sdk:10.0 versions matters

✅ A checklist to verify “yes, I can run this Dockerfile in my environment”

Copy‑paste friendly commands included. Let’s dissect this thing. 🪓

1. The Dockerfile We’re Analyzing (Big Picture)

Here’s the multi‑stage Dockerfile, slightly formatted:

# Base runtime stage (used for running the app)
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

# Build stage (used to compile the project)
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Directory.Packages.props", "."]
COPY ["Directory.Build.props", "."]
COPY ["Web.Api/Web.Api.csproj", "Web.Api/"]
RUN dotnet restore "./Web.Api/Web.Api.csproj"
COPY . .
WORKDIR "/src/Web.Api"
RUN dotnet build "./Web.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build

# Publish stage (produces the final published output)
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Web.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

# Final runtime stage (what actually runs in prod)
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Web.Api.dll"]

At a high level, this is a classic multi‑stage Dockerfile:

  1. base → ASP.NET runtime, non‑root user, ports exposed
  2. build → full .NET SDK, restores & compiles your Web API
  3. publish → takes the build output and publishes a trimmed app
  4. final → runtime image + published app + clean entrypoint

Let’s go stage by stage.

2. Stage‑by‑Stage Breakdown

2.1 Base Stage — Runtime Image

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

What this does:

  • Uses ASP.NET Core 9.0 runtime (Linux container).
  • Switches to a non‑root user via USER $APP_UID.
  • Sets /app as the working directory.
  • Exposes ports 8080 and 8081 (for HTTP/HTTPS or multiple endpoints).

Mental model:

This is the “slim, production‑ready base” where your app will run. No SDK, just runtime + your files.

2.2 Build Stage — SDK Image for Compiling

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Directory.Packages.props", "."]
COPY ["Directory.Build.props", "."]
COPY ["Web.Api/Web.Api.csproj", "Web.Api/"]
RUN dotnet restore "./Web.Api/Web.Api.csproj"
COPY . .
WORKDIR "/src/Web.Api"
RUN dotnet build "./Web.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build

Key points:

  • Uses .NET SDK 10.0 (preview/future version) to restore and build the project.
  • Assumes these files exist in the build context (folder where you call docker build):
  Directory.Packages.props
  Directory.Build.props
  Web.Api/Web.Api.csproj
  Web.Api/...
  • Flow:
    1. Copy minimal files (.props + .csproj) for faster restore caching.
    2. dotnet restore downloads all NuGet packages.
    3. Copy the rest of the source (COPY . .).
    4. Build the Web API project into /app/build.

Mental model:

This stage is your “build server inside a container”. It contains the full SDK and compiles your code, but it won’t be shipped as‑is to production.

2.3 Publish Stage — Final Output

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Web.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

What happens here:

  • Reuses the build stage (SDK + source + dependencies).
  • Runs dotnet publish to generate an optimized output into /app/publish.
  • Uses /p:UseAppHost=false to avoid bundling a platform‑specific executable; you’ll run with dotnet Web.Api.dll.

Mental model:

This stage transforms your compiled app into the final published bundle that will be copied into the runtime image.

2.4 Final Stage — What Actually Runs in Production

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Web.Api.dll"]

This is the stage that creates the image you actually run:

  • Starts from the base runtime image (ASP.NET 9.0, non‑root user, ports exposed).
  • Copies the published output from the publish stage.
  • Sets the entrypoint to:
  dotnet Web.Api.dll

Mental model:

Final image = runtime + published app. Small, clean, production‑oriented.

3. What You Must Have Installed to Build This Image

The good news: you don’t need .NET SDK installed on your host to build this image. The Dockerfile uses SDK images inside the container.

Mandatory

  1. Docker Engine / Docker Desktop

    • Windows, macOS, or Linux, with Linux containers enabled.
  2. Correct project layout matching the Dockerfile

    In the directory where you run docker build, you should see something like:

   Directory.Packages.props
   Directory.Build.props
   Web.Api/
       Web.Api.csproj
       Program.cs
       appsettings.json
       ...
   Dockerfile
  1. Internet access (at least for the first build) Docker must be able to pull:
  • mcr.microsoft.com/dotnet/aspnet:9.0
  • mcr.microsoft.com/dotnet/sdk:10.0 (or 9.0 if you align versions)
  • All NuGet packages during dotnet restore.

Optional (Nice to Have)

  • .NET SDK on your host Only needed if you also want to run:
  dotnet run
  dotnet test

directly on your machine. The Docker build itself doesn’t require it.

4. The USER $APP_UID Trap (And How to Fix It)

This line lives in the base stage:

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID

It tries to ensure your app does not run as root inside the container. Great for security.

But there’s a catch:

For this to work correctly:

  • The environment variable APP_UID must be set at build or runtime, and
  • That UID must map to a valid user inside the container.

If not, you can get:

  • Permission errors
  • “No such user” problems
  • Confusing runtime failures

✅ Option 1 — Easiest for Local Dev: Comment it Out

For local testing only, you can temporarily remove or comment the line:

# USER $APP_UID

Your app will run as root inside the container, which is fine for dev, but not ideal for production security.

✅ Option 2 — Define and Create a Non‑Root User

Hardened, production‑friendly version:

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base

ARG APP_UID=1000
ARG APP_GID=1000

RUN groupadd -g $APP_GID appgroup     && useradd -u $APP_UID -g $APP_GID -m appuser

USER appuser
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

Now you can optionally override the UID/GID at build time:

docker build -t my-webapi --build-arg APP_UID=1001 --build-arg APP_GID=1001 .

✅ Option 3 — Use a Predefined Non‑Root User (If Image Provides It)

Some images define built‑in users like dotnet or app. In that case, you might see:

USER app

But that depends on the specific tag and image; check the official docs for the image you’re using.

Recommended strategy:

  • Dev: comment USER $APP_UID if it’s blocking you.
  • Prod: properly define and create the non‑root user as shown above.

5. How to Build the Image (Step by Step)

In the folder where your Dockerfile and Web.Api project live, run:

# Basic build (uses default Release configuration)
docker build -t my-webapi .

Want to be explicit about configuration?

docker build -t my-webapi --build-arg BUILD_CONFIGURATION=Release .

What Docker will do:

  1. Pull mcr.microsoft.com/dotnet/sdk:10.0 (or from cache)
  2. Restore NuGet packages for Web.Api.csproj
  3. Build the project to /app/build
  4. Publish the project to /app/publish
  5. Create a final runtime image from aspnet:9.0 with /app/publish copied in

If the build fails, check:

  • Are Directory.Packages.props and Directory.Build.props really in the context?
  • Is the project folder exactly Web.Api and the file exactly Web.Api.csproj?
  • Do you need to fix or remove USER $APP_UID?

6. How to Run the Container

Once the image builds successfully:

docker run --rm -p 8080:8080 --name my-webapi my-webapi

What this means:

  • --rm → removes the container when it stops
  • -p 8080:8080host port 8080 → container port 8080
  • --name my-webapi → gives the container a readable name
  • my-webapi → the image you built

Now browse to:

  • http://localhost:8080
  • Maybe http://localhost:8080/swagger depending on your API setup.

What if your app listens on port 80 inside the container?

Sometimes ASP.NET is configured to listen on http://+:80 inside the container. In that case, change the mapping:

docker run --rm -p 8080:80 my-webapi
  • Host 8080 → Container 80

Tip:

Always confirm your Kestrel configuration (ASPNETCORE_URLS, appsettings.json, or Program.cs) to map ports correctly.

7. Version Alignment: aspnet:9.0 vs sdk:10.0

Right now the Dockerfile uses:

  • Runtime: mcr.microsoft.com/dotnet/aspnet:9.0
  • SDK: mcr.microsoft.com/dotnet/sdk:10.0

This can work (SDK 10 building a .NET 9 app), but in most real‑world setups you want matching major versions, e.g.:

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build

Why align versions?

  • Less surprise when APIs change between SDK versions
  • Consistent behavior between build and runtime
  • Easier upgrades: bump both to 10.0 at once later

Rule of thumb:

Unless you explicitly know why you need a newer SDK, keep runtime and SDK on the same major version.

8. Quick Checklist: “Can I Run This Dockerfile?”

Use this as a quick validation list before you start debugging in circles:

Environment

  • [ ] Docker Desktop / Engine is installed and running
  • [ ] You are using Linux containers, not Windows containers

Project Layout

  • [ ] Dockerfile is at the root of the solution (where you intend to build)
  • [ ] Directory.Packages.props and Directory.Build.props exist in that directory
  • [ ] There is a Web.Api folder with Web.Api.csproj and the API source files

Security/User

  • [ ] Either:
    • [ ] USER $APP_UID is temporarily commented out for dev, or
    • [ ] A valid non‑root user is created and APP_UID/APP_GID are configured

Build

  • [ ] docker build -t my-webapi . completes successfully
  • [ ] No dotnet restore errors (NuGet sources reachable, correct TFMs, etc.)

Run

  • [ ] docker run -p 8080:8080 my-webapi starts the container
  • [ ] The app responds at http://localhost:8080 (or mapped port/route)
  • [ ] Logs show the app listening on the expected URL/port

If all items are checked, you’re in a solid place to start iterating and hardening.

Final Thoughts

Visual Studio’s generated Dockerfile isn’t magic — it’s a clean example of a multi‑stage build:

  • base → runtime foundation
  • build → SDK and compilation
  • publish → final app output
  • final → minimal runtime image

Once you fully understand a Dockerfile like this, you can:

  • Tweak it for different projects (other Web APIs, gRPC, background workers)
  • Enforce non‑root users correctly in production
  • Align SDK/runtime versions with intent
  • Plug the same image into Kubernetes, Azure Container Apps, ECS, Cloud Run, etc.

If you’d like a follow‑up article, here are some natural next steps:

  • Multi‑stage builds with Node + .NET (SPA + API in one image)
  • Using multi‑arch images for ARM (Apple Silicon, Raspberry Pi)
  • Dockerfile patterns for minimal images (Alpine, distroless)
  • Integrating this image into CI/CD pipelines (GitHub Actions, Azure DevOps, GitLab CI)

✍️ Written for engineers who don’t just want Docker to “work”, but want to understand what’s happening in every layer.