## 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

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

- [Terraform](https://www.terraform.io/downloads.html) 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:

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

cd terraform/gcp/
```

#### Configure Backend (Optional)

If you want to use a [remote backend](https://developer.hashicorp.com/terraform/language/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:

```text
# 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:

```bash
terraform init
```

#### Plan the Deployment

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

```bash
terraform plan
```

#### Apply the Configuration

Apply the Terraform configuration to create the resources in GCP:

```bash
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.

```bash
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.

```bash
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.

```bash
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:

```bash
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:

```bash
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:

```bash
curl ifconfig.me
```

Update the master_authorized_networks in your terraform.tfvars configuration accordingly:

```text
master_authorized_networks = [

{

cidr_block   = "your-public-ip/32"

display_name = "Your Machine"

},

]
```

Reapply the Terraform configuration if you make changes:

```bash
terraform apply
```

#### Set up the Helm Repository

Add the Medplum Helm repository:

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

helm repo update
```

Generate a local `values.yaml` file:

```bash
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:

```yaml
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](/content/docs/self-hosting/install-on-gcp#generate-configuration-secret/index.html) step.

#### Edit service account values

```yaml
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)

```yaml
  ingress:

deploy: true

domain: api.yourdomain.com
```

Replace `api.yourdomain.com` with your actual domain.

#### Install the Application

```bash
helm install medplum medplum/medplum \

--namespace medplum \

--create-namespace \

-f values.yaml
```

- **Update DNS Records:**

Obtain the IP address of the Ingress:

```bash
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:

```bash
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:

```bash
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:

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

#### Post-Deployment Verification

**Check Backend API:**

Test the API endpoint:

```text
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](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:

```bash
tf destroy
```

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