Install on GCP | Medplum
High-level overview
To deploy Medplum in GCP, the process is divided into two parts:
- Static Infrastructure (using Terraform)
- Medplum App (Helm chart)
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
- The Medplum backend (API) container runs in GKE.
- The API is exposed using Ingress, which creates a GCP Load Balancer.
- The load balancer has a WAF (Cloud Armor).
- Google-managed certificates are used.
- We use managed Redis (Memorystore) and Cloud SQL for cache and PostgreSQL.
- All outbound connections go through a Cloud NAT (fixed IP).
- The external load balancer is deployed using Terraform and is used to expose the frontend (app) and storage buckets, using Google CDN.
- The load balancer has a WAF (Cloud Armor).
- Google-managed certificates are used.
High-level deployment process
- Deploy static infrastructure (GKE, CloudSQL, Redis, Storage Buckets, LB)
- With the values from Step 1, create the Medplum app configuration.
- With the values from Step 1, point the DNS records.
- Deploy the backend application using the Helm chart.
- 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
- Terraform installed on your local machine.
- A GCP account with the necessary permissions to create resources.
- A GCP project where the resources will be deployed.
- Google Cloud SDK installed and authenticated with your GCP account.
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.
- Update DNS Records: In your DNS provider’s management console, create A records pointing your domains to the load balancer’s IP address:
- For
storage.example.com: Create an A record for storage.example.com pointing to the load balancer’s IP. - For
app.example.com: Create an A record for app.example.com pointing to the same load balancer’s IP.
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:
- Deployment
- Horizontal Pod Autoscaler
- Service
- Ingress with GCP Managed Certificate and Backend Config (Cloud Armor)
- The ingress is optional. Users can choose to expose the API with other methods.
- Service Account
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
- Update DNS Records:
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.