Install on GCP | Medplum

High-level overview

To deploy Medplum in GCP, the process is divided into two parts:

This division allows a fully customizable deployment, for example: if a customer wants to use an existing GKE cluster, they can just deploy the Helm chart to it.

The Medplum application is configured using a secret in GCP Secrets Manager.

GCP Architecture

Infrastructure summary

High-level deployment process

  1. Deploy static infrastructure (GKE, CloudSQL, Redis, Storage Buckets, LB)
  2. With the values from Step 1, create the Medplum app configuration.
  3. With the values from Step 1, point the DNS records.
  4. Deploy the backend application using the Helm chart.
  5. Copy the frontend files to the CDN bucket.

GCP Deployment

Infrastructure Deployment (Terraform)

The terraform folder contains Terraform configurations for deploying infrastructure on Google Cloud Platform (GCP). The setup includes a Virtual Private Cloud (VPC), Google Kubernetes Engine (GKE) cluster, Cloud SQL, Cloud Storage Buckets, Redis, and more.

Prerequisites

Clone the Repository

Run:

git clone https://github.com/medplum/medplum

cd terraform/gcp/

Configure Backend (Optional)

If you want to use a remote backend for storing the Terraform state, uncomment and configure the backend.tf file.

Initialize Terraform

Modify the terraform.tfvars file to enter your project-specific values:

# GCP project configuration - Change these values to use your own project, region, and zone

project_id = "your-project-id"

region     = "your-region"

zone       = "your-zone"

# Common enforced labels - Change these values to use your own labels

labels = {

env     = "your-environment"  # e.g., "dev", "staging", "prod"

purpose = "your-purpose"      # e.g., "gke", "web", "database"

owner   = "your-owner"        # e.g., "team-name", "project-owner"

}

Initialize Terraform

Initialize the Terraform working directory to download the necessary provider plugins and modules:

terraform init

Plan the Deployment

Generate and review an execution plan to ensure the configuration is correct:

terraform plan

Apply the Configuration

Apply the Terraform configuration to create the resources in GCP:

terraform apply

Generate configuration secret

The configuration secret holds the Medplum application configuration and it contains the connection strings to the rest of the infrastructure that we deployed before, using Terraform.

1. Create the Secret in Secret Manager:

Use the gcloud secrets create command to create a new secret with automatic replication and labels.

gcloud secrets create config-secret --replication-policy="automatic"

2. Prepare the Secret Data:

Create a JSON file containing your secret data. Save it as secret_data.json.

cat <<EOF > secret_data.json

{

"port": 8103,

"baseUrl": "http://localhost:8103/",

"issuer": "http://localhost:8103/",

"audience": "http://localhost:8103/",

"jwksUrl": "http://localhost:8103/.well-known/jwks.json",

"authorizeUrl": "http://localhost:8103/oauth2/authorize",

"tokenUrl": "http://localhost:8103/oauth2/token",

"userInfoUrl": "http://localhost:8103/oauth2/userinfo",

"appBaseUrl": "http://localhost:3000/",

"binaryStorage": "file:./binary/",

"storageBaseUrl": "http://localhost:8103/storage/",

"supportEmail": "\"Medplum\" <support@medplum.com>",

"googleClientId": "397236612778-c0b5tnjv98frbo1tfuuha5vkme3cmq4s.apps.googleusercontent.com",

"googleClientSecret": "",

"recaptchaSiteKey": "6LfHdsYdAAAAAC0uLnnRrDrhcXnziiUwKd8VtLNq",

"recaptchaSecretKey": "6LfHdsYdAAAAAH9dN154jbJ3zpQife3xaiTvPChL",

"botLambdaRoleArn": "",

"botLambdaLayerName": "medplum-bot-layer",

"vmContextBotsEnabled": true,

"defaultBotRuntimeVersion": "vmcontext",

"allowedOrigins": "*",

"introspectionEnabled": true,

"database": {
    "host": "YOUR_DB_HOST",
    "port": 5432,
    "dbname": "medplum",
    "username": "medplum",
    "password": "medplum"
  },

"redis": {
    "host": "YOUR_REDIS_HOST",
    "port": 6379
  },

"bullmq": {
    "removeOnFail": { "count": 1 },
    "removeOnComplete": { "count": 1 }
  },

"shutdownTimeoutMilliseconds": 30000,

"chainedSearchWithReferenceTables": true
}

EOF

3. Add a New Secret Version with the Secret Data:

Use the gcloud secrets versions add command to add the secret data to your secret.

gcloud secrets versions add config-secret --data-file=secret_data.json

Configure DNS

After deploying the infrastructure, you need to point your domains to the external load balancer created by Terraform.

Obtain Load Balancer IP Address:

Retrieve the external IP address of the CDN external load balancer:

gcloud compute addresses list --global --filter="name=('medplum-elb')"

Note the IP address associated with medplum-elb.

This ensures that traffic to these domains is routed through the CDN-enabled load balancer, which serves content from your backend buckets configured in Terraform.

Deploy the Backend API Using Helm

The Medplum Helm chart is a package containing yaml templates representing Kubernetes objects.

It will deploy:

Configure kubectl

Get credentials for your GKE cluster:

gcloud container clusters get-credentials medplum-gke --region [MY_REGION] --project [MY_PROJECT_ID]

Replace [MY_REGION] and [MY_PROJECT_ID] with your actual values.

Note: Ensure your local machine’s public IP address is included in the master_authorized_networks in your Terraform GKE configuration to allow access to the cluster. To find your public IP address:

curl ifconfig.me

Update the master_authorized_networks in your terraform.tfvars configuration accordingly:

master_authorized_networks = [

{

cidr_block   = "your-public-ip/32"

display_name = "Your Machine"

},

]

Reapply the Terraform configuration if you make changes:

terraform apply

Set up the Helm Repository

Add the Medplum Helm repository:

helm repo add medplum https://charts.medplum.com

helm repo update

Generate a local values.yaml file:

helm show values medplum/medplum > values.yaml

Edit the values.yaml File

Edit the values.yaml file to override default values, specifying your cloud provider and configuration source:

global:

cloudProvider: gcp

configSource:

type: "gcp:[MY_PROJECT_ID]:[MY_CONFIG_SECRET_ID]"

Replace [MY_PROJECT_ID] with your actual GCP project ID.

Replace [MY_CONFIG_SECRET_ID] with the secret name created in the Generate configuration secret step.

Edit service account values

serviceAccount:

annotations:

iam.gke.io/gcp-service-account: [MY_GCP_SERVICE_ACCOUNT] # Your Google Cloud Platform service account e.g.: medplum-server@[MY_PROJECT_ID].iam.gserviceaccount.com

Replace [MY_GCP_SERVICE_ACCOUNT] with your actual GCP service account, which was created by Terraform and is named medplum-server@[MY_PROJECT_ID].iam.gserviceaccount.com.

Edit ingress values

(ingress is optional, customers can choose to use whatever method they like to expose the app)

  ingress:

deploy: true

domain: api.yourdomain.com

Replace api.yourdomain.com with your actual domain.

Install the Application

helm install medplum medplum/medplum \

--namespace medplum \

--create-namespace \

-f values.yaml

Obtain the IP address of the Ingress:

kubectl get ingress medplum --namespace medplum

NAME      CLASS    HOSTS                     ADDRESS        PORTS   AGE

medplum   <none>   api.yourdomain.com        34.8.101.254   80      18h

Update your DNS records to point api.yourdomain.com to the Ingress IP address.

This is the backend API endpoint.

Upgrade the backend application

Backend upgrades use the standard Medplum Helm upgrade process. The GCP-specific settings in values.yaml, such as the configuration source, service account annotations, and ingress settings, continue to apply during the upgrade. This upgrades the Kubernetes backend only; frontend static assets are built and uploaded separately in the next section.

Deploy the frontend (App)

Serve your frontend application through Cloud Storage and the CDN-enabled load balancer.

Build-time configuration

The Medplum app is a Vite-based single-page application. Environment variables are baked into the static build output at compile time — they are not read at runtime. You must set MEDPLUM_BASE_URL (and any other variables) before building.

Configure and build the app

From the root of the cloned Medplum repository, create packages/app/.env with your deployment values:

cat > packages/app/.env << 'EOF'

# Required: URL of your Medplum API server

MEDPLUM_BASE_URL=https://api.yourdomain.com/

# Optional: Pre-fill a specific OAuth2 client ID for all logins

MEDPLUM_CLIENT_ID=

# Optional: Enable Google Sign-In (provide your Google OAuth2 client ID)

GOOGLE_CLIENT_ID=

# Optional: Enable reCAPTCHA on the sign-in page (provide your reCAPTCHA v3 site key)

RECAPTCHA_SITE_KEY=

# Optional: Allow new users to self-register (set to "false" to disable)

MEDPLUM_REGISTER_ENABLED=true

# Optional: Enable AWS Textract integration

MEDPLUM_AWS_TEXTRACT_ENABLED=false

EOF

Replace https://api.yourdomain.com/ with your actual API domain. Then install dependencies and build:

npm ci --include dev

npm run build:fast

Upload to Cloud Storage

Use the deploy-app-gcp.sh script to upload the built static files to the CDN bucket. Replace medplum-static-assets with your actual GCS bucket name:

APP_BUCKET=medplum-static-assets ./scripts/deploy-app-gcp.sh

Post-Deployment Verification

Check Backend API:

Test the API endpoint:

curl https://api.yourdomain.com/health

You should receive a successful response indicating the API is operational.

Check Frontend Application:

Visit https://app.example.com in a web browser to ensure it’s serving correctly and interacting with the backend API.

Clean Up Resources (Optional)

If you need to tear down the infrastructure, use:

tf destroy

Note: This will destroy all resources created by Terraform, including the GKE cluster and load balancer.