> ## Documentation Index
> Fetch the complete documentation index at: https://docs.speckle.systems/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy to Kubernetes

> Deploy Speckle server to Kubernetes for enterprise and production environments

To ease new deployments, we are maintaining [a Kubernetes Helm chart](https://hub.docker.com/r/speckle/speckle-server-chart).

<Note>
  This setup is not recommended for use as-is in production for a few reasons, namely:

  * Application-level updates: We tend to move quite fast, and if things get busy, blink twice, and you’re on an outdated server. This has security implications, too.
  * Database backups: Again, this is up to you, mostly based on your risk appetite when it comes to dealing with live data. We’re healthily paranoid, so we’ve set up replication, failover nodes, and PITR.
  * Automatic scalability: for example, the preview service can be quite a monster; that setup can eat up all a vm’s resources, and starve other processes, causing general system-wide instability. Cloud providers can provide horizontal VM auto-scaling, and Kubernetes can provide horizontal pod auto-scaling, but these are not discussed in this guide.
  * Monitoring: this setup does not describe integrations for telemetry, metrics, tracing, or logging. Nor does it describe alerting or response actions.
  * Firewall and network hardening: running in production requires additional security hardening measures, particularly protecting the network from intrusion or data exfiltration.

  If you need help deploying a production server, [we can help](https://speckle.systems/pricing/)!
</Note>

## Prerequisites

* \[Required] A [DigitalOcean](https://www.digitalocean.com/) account. Please note that this guide will create resources that may incur a charge on DigitalOcean.
* \[Required] [Helm CLI](https://helm.sh/docs/intro/install/) installed.
* \[Required] [Kubernetes CLI client](https://kubernetes.io/docs/tasks/tools/#kubectl) installed.
* \[Required] A domain name, or a sub-domain to which a DNS A Record can be added.
* \[Optional] An email service provider account of your choice (to allow the server to send emails)
* \[Optional] An authentication service of your choice (to allow the server to authenticate users), otherwise username & password authentication will be used.
* \[Optional] The [DigitalOcean CLI client](https://docs.digitalocean.com/reference/doctl/how-to/install/) is installed.

<Steps>
  <Step>
    ## Create the Kubernetes Cluster

    Go to your DigitalOcean dashboard and [create a new Kubernetes cluster](https://cloud.digitalocean.com/kubernetes/clusters/new). We gave the cluster a name but left the configuration as per DigitalOcean's recommended defaults. When prompted to select the node count and size, we selected four nodes. Each node has the default 2 vCPU and 4 GB (`s-2vcpu-4gb`). While this is a minimum, your usage may vary, and we recommend testing under your typical loads and adjusting by deploying new nodes or larger-sized machines in new node-pools.

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/01_select_node_size.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=dfa24d7752349ba2b7f58c5e67570ca8" width="2084" height="2340" data-path="images/developers/server/kubernetes/01_select_node_size.png" />
    </Frame>
  </Step>

  <Step>
    Configure the other options for your Kubernetes cluster, then click the `Create Cluster` button. After the cluster is created and initialized, you should see it in your list of kubernetes clusters:

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/02_kubernetes_clusters.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=da690ff7e9a8e278a7a28f1080af96b8" width="2482" height="404" data-path="images/developers/server/kubernetes/02_kubernetes_clusters.png" />
    </Frame>
  </Step>

  <Step>
    To log in to the cluster, follow the getting-started guide on the DigitalOcean dashboard for your cluster. We recommend using the automated option to update your local Kubernetes configuration (kubeconfig) with the [DigitalOcean client, `doctl`](https://docs.digitalocean.com/reference/doctl/how-to/install/).

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/03_get_kubeconfig.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=f25b2caba424e2f01b612cf4569b2f52" width="2498" height="1876" data-path="images/developers/server/kubernetes/03_get_kubeconfig.png" />
    </Frame>
  </Step>

  <Step>
    After downloading the kubernetes config, you can verify that your kubernetes client has the cluster configuration by running the following command. A list of kubernetes clusters will be printed; your cluster context should have the prefix `do-`. Make a note of the name, you will use this in place of `${YOUR_CLUSTER_CONTEXT_NAME}` in most of the following steps of this guide.

    ```shell theme={null}
    kubectl config get-contexts
    ```

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/04_show_contexts.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=ed9c610e92ec3c6944dd03ff51d40ace" width="1982" height="138" data-path="images/developers/server/kubernetes/04_show_contexts.png" />
    </Frame>
  </Step>

  <Step>
    Verify that you can connect to the cluster using kubectl by running the following command to show the nodes you have provisioned. Remember to replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster.

    ```shell theme={null}
    kubectl get nodes --context "${YOUR_CLUSTER_CONTEXT_NAME}"
    ```

    You should see something like the following:

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/05_kubectl_get_nodes.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=3ada1c5b62f5a28ea4dc73daad8ae7da" width="1182" height="216" data-path="images/developers/server/kubernetes/05_kubectl_get_nodes.png" />
    </Frame>
  </Step>

  <Step>
    ### (Optional): Configure Valkey

    Speckle requires a Valkey cache to function. You can provide your own if you have an existing database. Otherwise, follow the steps below to create a new Valkey database on DigitalOcean.

    * We will deploy a managed Valkey provided by DigitalOcean. Go to the [new Database creation page](https://cloud.digitalocean.com/databases/new). Firstly, select the same region and VPC as you used when deploying your Kubernetes cluster, and select Valkey. Provide a name, and click `Create Database Cluster`.
      Again, we used the default sizes, but your usage will vary, and we recommend testing under your typical loads and adjusting as needed based on the database size.

          <Frame>
            <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/06_redis_configuration.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=3dde348e0f9df3f77e03af86a7bc67bf" width="1300" height="2280" data-path="images/developers/server/kubernetes/06_redis_configuration.png" />
          </Frame>

    * From the overview, click on `Secure this database cluster by restricting access.` This will take you to the Trusted Sources panel in the Settings tab. Here, we will improve the security of your database by only allowing connections from your Kubernetes cluster. Type the name of your Kubernetes cluster and add it as a Trusted Source.

          <Frame>
            <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/07_redis_trusted_source.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=53be28a266e0b17c98c29c483ef12b94" width="2554" height="1988" data-path="images/developers/server/kubernetes/07_redis_trusted_source.png" />
          </Frame>

    * In the Overview tab for your Valkey database. Select `connection string` from the dropdown, and copy the displayed Connection String. You will require this when configuring your deployment in [step 4](#step-4-configure-your-deployment).
          <Frame>
            <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/08_redis_connection_string.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=0ed2d67658af0225ce1ba589fa8dbf04" width="2502" height="1882" data-path="images/developers/server/kubernetes/08_redis_connection_string.png" />
          </Frame>
  </Step>

  <Step>
    ### (Optional): Configure Postgres

    Speckle requires a Postgres database to function. You can provide your own if you have an existing database. Otherwise, follow the following steps to create a new Postgres database in DigitalOcean.

    * We will now deploy a managed Postgres provided by DigitalOcean. Go to the [new Database creation page](https://cloud.digitalocean.com/databases/new). Firstly, select the same region and VPC as you used when deploying your Kubernetes cluster, and then select Postgres. Provide a name, and click `Create Database Cluster`.
      Again, we used the default sizes, but your usage will vary, and we recommend testing under your typical loads and adjusting as needed based on the database size.

          <Frame>
            <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/09_postgres_configuration.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=a674d8db86926302ae26a7e2488746b0" width="1294" height="2282" data-path="images/developers/server/kubernetes/09_postgres_configuration.png" />
          </Frame>

          <Frame>
            <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/09_postgres_configuration_02.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=a4c782790c2fd794ebaca91f382a14b4" width="1268" height="1366" data-path="images/developers/server/kubernetes/09_postgres_configuration_02.png" />
          </Frame>

    * From the overview page for your Postgres database, click on `Secure this database cluster by restricting access`. This will take you to the Trusted Sources panel in the Settings tab. Here, we will improve the security of your database by only allowing connections from your Kubernetes cluster. Type the name of your Kubernetes cluster and add it as a Trusted Source.

          <Frame>
            <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/10_postgres_trusted_source_edit.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=253626fb8fe271c255f429b02ffdbc59" width="2478" height="2070" data-path="images/developers/server/kubernetes/10_postgres_trusted_source_edit.png" />
          </Frame>

    * In the Overview tab for your Valkey database. Select `connection string` from the dropdown, and copy the displayed Connection String. You will require this for when configuring your deployment in [step 4](#step-4-configure-your-deployment).
          <Frame>
            <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/11_postgres_connection_string.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=406503d957f85ee2064c7bf383119bc6" width="2526" height="2256" data-path="images/developers/server/kubernetes/11_postgres_connection_string.png" />
          </Frame>
  </Step>

  <Step>
    ### (Optional): Configure Blob Storage (DigitalOcean Spaces)

    Speckle requires Blob Storage to store files and other data. You can provide your own if you have an existing blob storage which is compatible with the [Amazon S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html). Otherwise, follow the following steps to create a new S3-compatible blob storage on DigitalOcean.

    * Navigate to the [Create a Space page](https://cloud.digitalocean.com/spaces/new). Please select a region of your choice, we recommend the same region as you have deployed the cluster. We did not enable the CDN, and we restricted the file listing for security purposes. Please provide a name for your Space, this has to be unique in the region so please use a different name than our example. Make a note of this name; this is the `bucket` value, which we will require when configuring your deployment in subsequent steps. Click on `Create Space`.

          <Frame>
            <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/12_blob_storage_configuration.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=1bbb2015d7ad87312c2aeed4ba62de6f" width="2424" height="2434" data-path="images/developers/server/kubernetes/12_blob_storage_configuration.png" />
          </Frame>

    * Once created, click on the `Settings` tab and add a `CORS Configurations`.

    * Add a CORS Configuration which allows `PUT` requests from your domain.

    <Frame>
      <img src="https://mintcdn.com/speckle/tH1u1dKYaNtn7MaZ/images/developers/server/kubernetes/38_spaces_cors_configuration.png?fit=max&auto=format&n=tH1u1dKYaNtn7MaZ&q=85&s=adbeb33a0e5f4f19f20efb5546c9e90f" width="1178" height="1330" data-path="images/developers/server/kubernetes/38_spaces_cors_configuration.png" />
    </Frame>

    * Now click on the `Settings` tab and copy the `Endpoint` value.

          <Frame>
            <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/13_spaces_endpoint.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=25cd1aeb14d5bca18809d03f8d66da41" width="2520" height="1722" data-path="images/developers/server/kubernetes/13_spaces_endpoint.png" />
          </Frame>

    * Now navigate to the [API page](https://cloud.digitalocean.com/account/api/tokens) in DigitalOcean. Next to the `Spaces access keys` heading, click `Generate New Key`. You will only be able to see the Secret value once, so copy the name, the key and the secret and store this securely.
          <Frame>
            <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/14_spaces_access_key.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=19d19744bb3fda9e87e809d1a0d8e341" width="2496" height="400" data-path="images/developers/server/kubernetes/14_spaces_access_key.png" />
          </Frame>
  </Step>

  <Step>
    ### Create a Namespace

    Kubernetes allows applications to be separated into different namespaces. We can create a namespace in our Kubernetes cluster with the following command. Replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster.:

    ```shell theme={null}
    kubectl create namespace speckle --context "${YOUR_CLUSTER_CONTEXT_NAME}"
    ```

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/15_create_namespace.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=b08cb6789892f1414a96bbe4c7e0da84" width="1468" height="74" data-path="images/developers/server/kubernetes/15_create_namespace.png" />
    </Frame>
  </Step>

  <Step>
    Verify that the namespace was created by running the following command. You should see a list of namespaces, including `speckle`. The other existing namespaces were created by Kubernetes and are required for Kubernetes to run. Replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster.

    ```shell theme={null}
    kubectl get namespace --context "${YOUR_CLUSTER_CONTEXT_NAME}"
    ```

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/16_get_namespaces.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=f4e4f67d673204572496785a1f439408" width="1294" height="254" data-path="images/developers/server/kubernetes/16_get_namespaces.png" />
    </Frame>
  </Step>

  <Step>
    ### Create Secrets

    To securely store the connection details for Speckle's dependencies, we will create a secret in the Kubernetes Cluster's `speckle` namespace. Replace all the items starting with `${YOUR_...}` with the appropriate value. `${YOUR_SECRET}` should be replaced with a value unique to this cluster. We recommend creating a random value of at least 10 characters long.

    ```shell theme={null}
    kubectl create secret generic server-vars \
     --context "${YOUR_CLUSTER_CONTEXT_NAME}" \
     --namespace speckle \
     --from-literal=redis_url="${YOUR_REDIS_CONNECTION_STRING}" \
     --from-literal=postgres_url="${YOUR_POSTGRES_CONNECTION_STRING}" \
     --from-literal=s3_secret_key="${YOUR_SPACES_SECRET}" \
     --from-literal=session_secret="${YOUR_SECRET}" \
     --from-literal=email_password="${YOUR_EMAIL_SERVER_PASSWORD}" # optional, only required if you wish to enable email invitations
    ```

    You can verify that your secret was created correctly by running the following command. Replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster.:

    ```shell theme={null}
    kubectl describe secret server-vars --namespace speckle --context "${YOUR_CLUSTER_CONTEXT_NAME}"
    ```

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/17_secrets.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=2ff89457fe0decabe4a4b1fd76bbab2c" width="1782" height="114" data-path="images/developers/server/kubernetes/17_secrets.png" />
    </Frame>

    To view the contents of an individual secret, you can run the following, replacing `redis_url` with the key you require and replacing `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster.:

    ```shell theme={null}
    kubectl get secret server-vars --context "${YOUR_CLUSTER_CONTEXT_NAME}" \
      --namespace speckle \
      --output jsonpath='{.data.redis_url}' | base64 --decode
    ```

    Should you need to amend any values after creating the secret, use the following command. More information about working with secrets can be found on the [Kubernetes website](https://kubernetes.io/docs/concepts/configuration/secret/#editing-a-secret). Replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster.

    ```shell theme={null}
    kubectl edit secrets server-vars --namespace speckle --context "${YOUR_CLUSTER_CONTEXT_NAME}"
    ```
  </Step>

  <Step>
    ### Priority Classes

    If Kubernetes ever begins to run out of resources (such as processor or memory) on a node, then Kubernetes will have to terminate some of the processes. Kubernetes decides which processes to terminate based on their priority. Here we will specify the priority that Speckle will have.

    Run the following command. Replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster.:

    ```shell theme={null}
    cat <<'EOF' | kubectl create --context "${YOUR_CLUSTER_CONTEXT_NAME}" --namespace speckle --filename -
    apiVersion: scheduling.k8s.io/v1
    kind: PriorityClass
    metadata:
      name: high-priority
    value: 100
    globalDefault: false
    description: "High priority (100) for business-critical services."
    apiVersion: scheduling.k8s.io/v1
    kind: PriorityClass
    metadata:
      name: medium-priority
    value: 50
    globalDefault: true
    description: "Medium priority (50) - dev/test services."
    apiVersion: scheduling.k8s.io/v1
    kind: PriorityClass
    metadata:
      name: low-priority
    value: -100
    globalDefault: false
    description: "Low priority (-100) - Non-critical microservices."
    EOF
    ```

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/18_priority_classes.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=8e5e480b5537e3423250fb9585277fd7" width="2076" height="1004" data-path="images/developers/server/kubernetes/18_priority_classes.png" />
    </Frame>
  </Step>

  <Step>
    ### Certificate Manager

    To enable secure (https) access to your Speckle server from the internet, we need to provide a means to create a TLS (X.509) certificate. This certificate must be renewed and kept up to date. To automate this, we will install CertManager and connect it to a Certificate Authority. CertManager will create a new certificate, request that the Certificate Authority sign it, and renew it when required. The Certificate Authority in our case will be [Let's Encrypt](https://letsencrypt.org/). If you are interested, you can read more about how Let's Encrypt knows to trust your server with an [HTTP-01 challenge](https://letsencrypt.org/docs/challenge-types/#http-01-challenge), in our case, CertManager acts as the ACME client.

    We first need to let Helm know where CertManager can be found:

    ```shell theme={null}
    helm repo add jetstack https://charts.jetstack.io
    ```

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/19_helm_jetstack_repo.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=4cc5eacf9a5e7381c988bcaef1f2110f" width="1118" height="72" data-path="images/developers/server/kubernetes/19_helm_jetstack_repo.png" />
    </Frame>
  </Step>

  <Step>
    Then update Helm, so it knows what the newly added repo contains

    ```shell theme={null}
    helm repo update
    ```
  </Step>

  <Step>
    Deploy the CertManager Helm release with the following command. Replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster.

    ```shell theme={null}
    helm upgrade cert-manager jetstack/cert-manager --namespace cert-manager --version v1.8.0 --set installCRDs=true --install --create-namespace --kube-context "${YOUR_CLUSTER_CONTEXT_NAME}"
    ```

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/20_certmanager.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=202be860c2eb762e08f51820f826244b" width="2856" height="900" data-path="images/developers/server/kubernetes/20_certmanager.png" />
    </Frame>
  </Step>

  <Step>
    We can verify that this was deployed to Kubernetes with the following command. Replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster.:

    ```shell theme={null}
    kubectl get pods --namespace cert-manager --context "${YOUR_CLUSTER_CONTEXT_NAME}"
    ```

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/21_certmanager_pods.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=fe2c1bf120780adf3cdacca60fc752cb" width="1640" height="188" data-path="images/developers/server/kubernetes/21_certmanager_pods.png" />
    </Frame>
  </Step>

  <Step>
    We now need to tell CertManager which Certificate Authority should be issuing the certificate. We will deploy a CertIssuer. Run the following command, replacing `${YOUR_EMAIL_ADDRESS}` and `${YOUR_CLUSTER_CONTEXT_NAME}` with the appropriate values.

    ```shell theme={null}
    cat <<'EOF' | kubectl apply --context "${YOUR_CLUSTER_CONTEXT_NAME}" --namespace cert-manager --filename -
    apiVersion: cert-manager.io/v1
    kind: ClusterIssuer
    metadata:
      name: letsencrypt-staging
    spec:
      acme:
        # The ACME server URL
        server: https://acme-staging-v02.api.letsencrypt.org/directory
        # Email address used for ACME registration
        email: ${YOUR_EMAIL_ADDRESS}
        # Name of a secret used to store the ACME account private key
        privateKeySecretRef:
          name: letsencrypt-staging
        # Enable the HTTP-01 challenge provider
        solvers:
        - http01:
            ingress:
              class:  nginx
    apiVersion: cert-manager.io/v1
    kind: ClusterIssuer
    metadata:
      name: letsencrypt-prod
    spec:
      acme:
        # The ACME server URL
        server: https://acme-v02.api.letsencrypt.org/directory
        # Email address used for ACME registration
        email: ${YOUR_EMAIL_ADDRESS}
        # Name of a secret used to store the ACME account private key
        privateKeySecretRef:
          name: letsencrypt-prod
        # Enable the HTTP-01 challenge provider
        solvers:
        - http01:
            ingress:
              class:  nginx
    EOF
    ```

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/22_certmanager_cluster_issuer.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=83d3c675dc2dd2430a7de52d7f764024" width="2152" height="752" data-path="images/developers/server/kubernetes/22_certmanager_cluster_issuer.png" />
    </Frame>
  </Step>

  <Step>
    We can verify that this worked by running the following command. Replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster. The response should state that the message was "*The ACME account was registered with the ACME server*".

    ```shell theme={null}
    kubectl describe clusterissuer.cert-manager.io/letsencrypt-staging \
     --namespace cert-manager --context "${YOUR_CLUSTER_CONTEXT_NAME}"
    ```

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/23_certmanager_account_registered.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=39333de35d4c6158fb6f5b8f3dc4e9c9" width="1718" height="442" data-path="images/developers/server/kubernetes/23_certmanager_account_registered.png" />
    </Frame>
  </Step>

  <Step>
    We repeat this command to verify that the production certificate was created as well. Replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster. Again, the response should state that the message was "*The ACME account was registered with the ACME server*".

    ```shell theme={null}
    kubectl describe clusterissuer.cert-manager.io/letsencrypt-prod \
     --namespace cert-manager --context "${YOUR_CLUSTER_CONTEXT_NAME}"
    ```
  </Step>

  <Step>
    ### Ingress

    To allow access from the internet to your kubernetes cluster, Speckle will deploy a [Kubernetes Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/), which defines how that external traffic should be managed. The component that manages the traffic per Speckle's ingress definition is known as an Ingress Controller. In this step, we will deploy our Ingress Controller, [NGINX](https://www.nginx.com/).

    * We first let Helm know where NGINX ingress can be found:

    ```shell theme={null}
    helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
    ```

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/24_helm_nginx_repo.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=d08e8fe9683ab83d13e264e7b78f0566" width="1522" height="68" data-path="images/developers/server/kubernetes/24_helm_nginx_repo.png" />
    </Frame>
  </Step>

  <Step>
    Then update Helm so that it can discover what the newly added repo contains:

    ```shell theme={null}
    helm repo update
    ```

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/25_helm_repo_update.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=3b68a22ab290e50728426cda6b8ee00d" width="1458" height="332" data-path="images/developers/server/kubernetes/25_helm_repo_update.png" />
    </Frame>
  </Step>

  <Step>
    Now we can deploy NGINX to our kubernetes cluster. The additional annotation allows CertManager, deployed in the [previous step](#3d-certificate-manager), to advise NGINX which certificate to use for https connections. Replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster.

    ```shell theme={null}
    cat <<'EOF' | helm upgrade ingress-nginx ingress-nginx/ingress-nginx \
            --install --create-namespace \
            --set-string controller.podAnnotations."acme.cert-manager.io/http01-edit-in-place"=true \
            --namespace ingress-nginx \
            --kube-context "${YOUR_CLUSTER_CONTEXT_NAME}" \
            --values -
    controller:
      replicaCount: 2
      publishService:
        enabled: true
      config:
        http2-max-concurrent-streams: "512"
        use-http2: "true"
        keep-alive-requests: "1000"
    EOF
    ```

    <Frame>
      <img src="https://mintcdn.com/speckle/EDPQP7dU1ux-cVtZ/images/developers/server/kubernetes/26_ingress_controller.png?fit=max&auto=format&n=EDPQP7dU1ux-cVtZ&q=85&s=8e2c002f0c77aed4e6180ef74f2e5b0e" width="2084" height="1250" data-path="images/developers/server/kubernetes/26_ingress_controller.png" />
    </Frame>

    <Note>
      We can ignore the instructions printed out by the NGINX Helm chart as the
      required resources will be provided by the Speckle Helm chart.
    </Note>
  </Step>

  <Step>
    ## Configure your Deployment

    [Download the `values.yaml` file from the Speckle server GitHub repository](https://raw.githubusercontent.com/specklesystems/speckle-server/refs/heads/main/utils/helm/speckle-server/values.yaml) and save it as `values.yaml` to the current directory on your local machine. We will edit and use this file in the following steps.
  </Step>

  <Step>
    * Fill in the requested fields and save the file:
      * `namespace`: required, we are using `speckle` in this guide, so change this value
      * `domain`: required, this is the domain name at which your Speckle server will be available.
      * `db.useCertificate`: required, this should be set to true and will force Speckle to use the certificate for Postgres we shall provide in `db.certificate`.
      * `db.certificate`: required, this can be found by clicking `Download CA certificate` in your database's overview page on DigitalOcean. You can find your Postgres database by selecting it from the [Database page on DigitalOcean](https://cloud.digitalocean.com/databases). When entering the data, please use Helm's pipe operator for multiline strings and be careful with indentation. We recommend reading [Helm's guide on formatting multiline strings](https://helm.sh/docs/chart_template_guide/yaml_techniques/#strings-in-yaml), and refer to the image below for an example of this format.
      * `s3.endpoint`: required, the endpoint can be found in the Settings Page of your DigitalOcean Space. You can find your Space by selecting it from the [Spaces page on DigitalOcean](https://cloud.digitalocean.com/spaces?i=b49c54). This value must be prepended with `https://`.
      * `s3.bucket`: required, this is the name of your DigitalOcean space.
      * `s3.access_key`: required, this is the `Key` of your Spaces API key. You can find this by viewing it from the [Spaces API Key page on DigitalOcean](https://cloud.digitalocean.com/account/api/tokens)
      * `s3.auth.local.enabled`: this is enabled by default. This requires users to register on your Speckle cluster with a username and password. If you wish to use a different authorization provider, such as Azure AD, GitHub, or Google, set this value to `false` and amend the relevant section below by enabling it and providing the necessary details.
      * `server.email`: optional, enabling emails will enable extra features like sending invites.
        * You will need to set `server.email.enabled` to `true`.
        * Please set `server.email.host`, `server.email.username`, and optionally, depending on your email server, `server.email.port`
        * This also requires the `email_password` secret to have been set in [Step 3](#step-3b-create-secrets).
      * `cert_manager_issuer`: optional, the default is set for Let's Encrypt staging api `letsencrypt-staging`. For production, or if you encounter an issue with certificates, change the value to `letsencrypt-prod`.

    The remaining values can be left as their defaults.

    <Frame>
      <img src="https://mintcdn.com/speckle/tH1u1dKYaNtn7MaZ/images/developers/server/kubernetes/37_values_yaml_showing_certificate_formatting.png?fit=max&auto=format&n=tH1u1dKYaNtn7MaZ&q=85&s=ca334fe18957fb2ef655efd6f2684c41" width="1284" height="592" data-path="images/developers/server/kubernetes/37_values_yaml_showing_certificate_formatting.png" />
    </Frame>
  </Step>

  <Step>
    ## Deploy Speckle to Kubernetes

    Run the following command to deploy the Helm chart to your Kubernetes cluster configured with the values you configured in the [prior step](#step-4-configure-your-deployment). Replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster.

    ```shell theme={null}
    helm upgrade my-speckle-server oci://registry-1.docker.io/speckle/speckle-server \
     --values values.yaml \
     --namespace speckle \
     --install --create-namespace \
     --kube-context "${YOUR_CLUSTER_CONTEXT_NAME}"
    ```

    After configuration is done, you should see this success message:

    <Frame>
      <img src="https://mintcdn.com/speckle/tH1u1dKYaNtn7MaZ/images/developers/server/kubernetes/29_install_speckle_release.png?fit=max&auto=format&n=tH1u1dKYaNtn7MaZ&q=85&s=c0f65ee431b5c7007dd51b4cd627e3fb" width="1252" height="394" data-path="images/developers/server/kubernetes/29_install_speckle_release.png" />
    </Frame>
  </Step>

  <Step>
    Verify that all deployed Helm charts were successful by checking their deployment status. Replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster.:

    ```shell theme={null}
    helm list --all-namespaces --kube-context "${YOUR_CLUSTER_CONTEXT_NAME}"
    ```

    You should see something similar to the following:

    <Frame>
      <img src="https://mintcdn.com/speckle/tH1u1dKYaNtn7MaZ/images/developers/server/kubernetes/30_helm_release_verification.png?fit=max&auto=format&n=tH1u1dKYaNtn7MaZ&q=85&s=ef42ed1bce2a4930635fcefaba0cedf0" width="2688" height="184" data-path="images/developers/server/kubernetes/30_helm_release_verification.png" />
    </Frame>
  </Step>

  <Step>
    ## Update your Domain

    Initially, accessing Speckle may take some time as DigitalOcean must create a load balancer and Let's Encrypt must sign the Certificate. The DigitalOcean load balancer was automatically requested from the Infrastructure provider (DigitalOcean) by the Ingress controller we deployed earlier. You can see the progress of the load balancer's deployment on the [Networking page](https://cloud.digitalocean.com/networking/load_balancers) of your DigitalOcean dashboard.
  </Step>

  <Step>
    Once the load balancer has finished creating, DigitalOcean will display an externally-accessible IP address for it. Please make a note of the IP address.

    <Frame>
      <img src="https://mintcdn.com/speckle/tH1u1dKYaNtn7MaZ/images/developers/server/kubernetes/31_get_IP_address.png?fit=max&auto=format&n=tH1u1dKYaNtn7MaZ&q=85&s=f30410aca44fb7a37d34ddc81bb826fa" width="1346" height="310" data-path="images/developers/server/kubernetes/31_get_IP_address.png" />
    </Frame>
  </Step>

  <Step>
    Navigate to your domain registrar's website for your domain name and add a DNS A record. This will allow web browsers to resolve your domain name to the load balancer's IP address. The domain must match the domain name provided to Speckle in the `values.yaml` file you edited previously. If DigitalOcean manages your Domain Names, adding a DNS A record using [DigitalOcean's Domain page](https://cloud.digitalocean.com/networking/domains) will look something like the following:

    <Frame>
      <img src="https://mintcdn.com/speckle/tH1u1dKYaNtn7MaZ/images/developers/server/kubernetes/32_domain_a_record.png?fit=max&auto=format&n=tH1u1dKYaNtn7MaZ&q=85&s=cd77499d6e23dee71b6136d9e06cb676" width="2482" height="1032" data-path="images/developers/server/kubernetes/32_domain_a_record.png" />
    </Frame>
  </Step>

  <Step>
    It may take a moment for the domain name and A Record to be propagated to all relevant DNS servers, and then for Let's Encrypt to reach your domain and generate a certificate. Please be patient while this is updated.
  </Step>

  <Step>
    ## Create an account on your Server

    You should be able to now visit your domain name and see the same Speckle welcome page.

    Finally, register the first user. The first user who registers will be the administrator account for that server.

    <Frame>
      <img src="https://mintcdn.com/speckle/tH1u1dKYaNtn7MaZ/images/developers/server/kubernetes/33_registration_page.png?fit=max&auto=format&n=tH1u1dKYaNtn7MaZ&q=85&s=a205d4e0a7fd54d53302d445f918b81f" width="3074" height="1332" data-path="images/developers/server/kubernetes/33_registration_page.png" />
    </Frame>
  </Step>
</Steps>

## That's it

You have deployed a Speckle Server on a fully controlled Kubernetes cluster.

To reconfigure the server, you can change the values in `values.yaml` and run the following command. Replace `${YOUR_CLUSTER_CONTEXT_NAME}` with the name of your cluster.:

```shell theme={null}
helm upgrade my-speckle-server --values values.yaml --kube-context "${YOUR_CLUSTER_CONTEXT_NAME}"
```

## Common Issues

### Untrusted Certificate

Your browser may not trust the certificate generated by Let's Encrypt's staging API. In Google's Chrome browser, the warning will appear as follows:

<Frame>
  <img src="https://mintcdn.com/speckle/tH1u1dKYaNtn7MaZ/images/developers/server/kubernetes/34_browser_warning.png?fit=max&auto=format&n=tH1u1dKYaNtn7MaZ&q=85&s=41817e5ddfbc49ef6132a11fcb20152d" width="1444" height="1532" data-path="images/developers/server/kubernetes/34_browser_warning.png" />
</Frame>

You can verify that the certificate was generated correctly by inspecting the Certificate's Issuing Authority. If the Certificate was correctly generated, the root certificate should be one of either `(STAGING) Pretend Pear X1` and/or `(STAGING) Bogus Broccoli X2`. Click the `Not Secure` warning next to the address bar, then click `Certificate is not valid` for more details.

<Frame>
  <img src="https://mintcdn.com/speckle/tH1u1dKYaNtn7MaZ/images/developers/server/kubernetes/35_inspecting_certificates.png?fit=max&auto=format&n=tH1u1dKYaNtn7MaZ&q=85&s=c7eeabc9ec17136cb88a0b68d0b59303" width="686" height="698" data-path="images/developers/server/kubernetes/35_inspecting_certificates.png" />
</Frame>

<Frame>
  <img src="https://mintcdn.com/speckle/tH1u1dKYaNtn7MaZ/images/developers/server/kubernetes/36_certificate_details.png?fit=max&auto=format&n=tH1u1dKYaNtn7MaZ&q=85&s=513b4d4d963ca5bf07f3e4a6bc4222ed" width="972" height="550" data-path="images/developers/server/kubernetes/36_certificate_details.png" />
</Frame>

In this case, our deployment is correct, but our browser rightly does not trust Let's Encrypt's staging environment. To resolve this issue, we recommend amending the Certificate to a production certificate. Please refer to the notes above on how to amend your Speckle deployment to use Let's Encrypt's Production environment.

More information about Let's Encrypt's Staging Environment can be found on [Let's Encrypt's website](https://letsencrypt.org/docs/staging-environment/#root-certificates).

### Other Issues

If you encounter any other issue, have any question or just want to say hi, reach out in [our forum](https://speckle.community/).
