---
source: https://carbone.io/documentation/developer/self-hosted-deployment/deploy-on-kubernetes.html
title: "Deploy on Kubernetes"
description: "Run your own Carbone instance in your Kubernetes cluster"
generated_at: "2026-07-24"
---

# Deploy on Kubernetes

Run your own Carbone instance in your Kubernetes cluster

## Introduction

Setting up a document generation solution in your Kubernetes cluster is simple.

> ℹ️ **Note:** The recommended way to deploy Carbone on Kubernetes is our official **Helm chart**, available on [Artifact Hub](https://artifacthub.io/packages/helm/carbone/carbone-ee). It handles the Deployment, Service, Secrets, Ingress, autoscaling and multi-instance peer discovery for you, and comes with ready-to-use `values.yaml` examples for AWS EKS, Azure AKS, GCP, OVH and Scaleway.

> ℹ️ **Note:** By default, the program runs with free Community features. To use Carbone Enterprise Edition, you need a Carbone license. [Contact us](https://carboneio.pipedrive.com/scheduler/5Rzzbxu6/carbone-on-premiseaws-presentation) to learn more, or request a free 30-day trial in our chat.

## Quickstart with Helm

### Prerequisites

-   Kubernetes 1.23+
-   Helm 3.8+
-   An ingress controller (nginx, traefik…) if external access is required
-   A persistent storage backend (S3, Azure Blob, or a `ReadWriteMany` PersistentVolume) for production use

### Installation

Add the Carbone Helm repository:

```bash
helm repo add carbone https://bin.carbone.io/helm/
helm repo update
```

Create a `values.yaml` file with at least your license:

```yaml
applicationConfiguration:
  license: "<YOUR_CARBONE_LICENSE>"
```

Install the chart:

```bash
helm upgrade --install carbone-ee carbone/carbone-ee \n  --create-namespace -n carbone \n  -f values.yaml
```

Then check that everything is running correctly:

```bash
kubectl get pods -n carbone
helm test carbone-ee -n carbone
```

> ℹ️ **Note:** The chart exposes `NOTES.txt` on install/upgrade, summarizing your scaling, storage, security and access configuration, and warning you about anything missing (license, storage, authentication...).

## Configuration reference

The recommended approach is to maintain a `values.yaml` file and pass it with `-f`. The key parameters are listed below; see the [full values.yaml on Artifact Hub](https://artifacthub.io/packages/helm/carbone/carbone-ee?modal=values) for everything else.

### Application

| Parameter | Description | Default |
| --- | --- | --- |
| `image.tag` | Carbone image tag | chart `appVersion` |
| `image.pullPolicy` | Image pull policy | `Always` |
| `replicaCount` | Number of replicas | `4` |
| `applicationConfiguration.license` | Carbone EE license key | `""` |
| `applicationConfiguration.port` | HTTP port | `4000` |
| `applicationConfiguration.studio` | Enable Carbone Studio UI | `true` |
| `applicationConfiguration.studioBasicAuthentication` | Basic auth for Studio (`user:password`) | `""` |
| `applicationConfiguration.authentication` | Enable JWT authentication on the API | `false` |
| `applicationConfiguration.authenticationPublicKey` | RSA public key for JWT verification | `""` |
| `applicationConfiguration.lang` | Default locale | `fr` |
| `applicationConfiguration.timezone` | Default timezone | `Europe/Paris` |
| `applicationConfiguration.nbConvertThread` | Number of LibreOffice conversion threads per pod | `1` |
| `applicationConfiguration.timeoutConversion` | Conversion timeout in ms | `60000` |
| `applicationConfiguration.maxInputSize` | Max request body size in bytes | `62914560` |
| `applicationConfiguration.templateManagement` | Enable the [template management](/documentation/developer/on-premise-installation/template-management.md) API | `true` |

The license and public key are never written in plain YAML in the cluster: the chart stores them in a Kubernetes `Secret` and injects them into the pods as environment variables.

### Autoscaling

| Parameter | Description | Default |
| --- | --- | --- |
| `autoscaling.enabled` | Enable HorizontalPodAutoscaler | `false` |
| `autoscaling.minReplicas` | Minimum replicas | `1` |
| `autoscaling.maxReplicas` | Maximum replicas | `100` |
| `autoscaling.targetCPUUtilizationPercentage` | CPU target for scaling | `70` |

### Ingress

| Parameter | Description | Default |
| --- | --- | --- |
| `ingress.enabled` | Enable ingress | `true` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | `{}` |
| `ingress.hosts` | List of hosts and paths | `[{host: "", paths: [{path: /}]}]` |
| `ingress.tls` | TLS configuration | `[]` |

## Storage backends

A persistent storage backend is required for production: templates and renders need to survive pod restarts and be shared across replicas. Choose **one** of the following options in your `values.yaml`.

### S3 (or S3-compatible)

Works with AWS S3, Scaleway Object Storage, OVH Object Storage, GCS (S3-compatible mode), MinIO, and others.

```yaml
persistentStorage:
  s3:
    enabled: true
    endpoint: s3.eu-west-1.amazonaws.com
    region: eu-west-1
    templatesBucket: my-carbone-templates
    rendersBucket: my-carbone-renders
    accessKeyId: <ACCESS_KEY_ID>
    accessKeySecret: <ACCESS_KEY_SECRET>
```

### Azure Blob Storage

```yaml
persistentStorage:
  azureBlobStorage:
    enabled: true
    storageAccount: mystorageaccount
    storageKey: <STORAGE_KEY>
    templatesContainer: carbone-templates
    rendersContainer: carbone-renders
```

### PersistentVolume (ReadWriteMany)

Suitable for on-premise or single-node setups. For multi-replica deployments, the volume must support `ReadWriteMany` (NFS is often the best solution).

```yaml
persistentStorage:
  persistentVolume:
    enabled: true
    persistentVolumeClaimName: carbone-pvc
    templateFolder: templates
    rendersFolder: renders
```

## Multi-instance and high availability

When `replicaCount > 1` or `autoscaling.enabled: true`, pods automatically discover each other via WebSocket (port 5001) and synchronize [template metadata](/documentation/developer/on-premise-installation/ha-and-scaling.md) — no additional configuration is required, peer discovery is handled by the headless service.

For optimal availability with multiple replicas, consider adding topology spread constraints to your `values.yaml`:

```yaml
affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: carbone-ee
          topologyKey: kubernetes.io/hostname
```

## Authentication

To enable API authentication:

**\- Generate a private/public Carbone key pair**

The key generation tool is included in the docker image from Carbone version 5 onwards.

```sh
docker run -it --platform "linux/amd64" carbone/carbone-ee:slim-5.0.0-beta.0 generate-keys
```

The two keys are generated and displayed in the console. Keep the private key (`key.pem`) secret on your side.

**\- Enable authentication in `values.yaml`**

```yaml
applicationConfiguration:
  authentication: true
  authenticationPublicKey: |
    <content of the public key>
```

Then upgrade your release with `helm upgrade` (see below). The chart stores the public key in a Kubernetes `Secret` and mounts it for you.

**\- Generate a JWT token**

```sh
docker run -it --platform "linux/amd64" carbone/carbone-ee:slim-5.0.0-beta.0 generate-token

## Paste the content of key.pem in the terminal
```

A JWT token is then displayed in the console. You can use it in your API calls.

## Upgrade and uninstall

```bash
helm upgrade carbone-ee carbone/carbone-ee -n carbone -f values.yaml
```

To uninstall:

```bash
helm uninstall carbone-ee -n carbone
kubectl delete namespace carbone
```

## Manual deployment (without Helm)

If you cannot use Helm, you can deploy Carbone with plain Kubernetes manifests instead.

> ⚠️ **Warning:** The example below uses a plain `Deployment`, which only supports simple, stateless setups. If you need [template management](/documentation/developer/on-premise-installation/template-management.md) or the [job balancer](/documentation/developer/on-premise-installation/ha-and-scaling.md#job-balancer) (multi-pod clustering, metadata replication), Carbone pods need stable network identities to discover and stay connected to each other, which requires a `StatefulSet` with a headless `Service` instead of a `Deployment`. Writing and maintaining that setup by hand is significantly more involved — **using the [Helm chart](#quickstart-with-helm) is recommended** in that case, since it already deploys a `StatefulSet` and wires up peer discovery for you.

### Storing Carbone secrets

The first thing is to store the secrets that will be used by Carbone. In this simple example, only the license is needed, but you also need to store the public key if authentication is enabled.

To store the license :

```bash
export CARBONE_LICENSE=`cat your-license.carbone-license`

kubectl create secret generic carbone-license --from-literal=license=${CARBONE_LICENSE}
```

To check that it has been taken into account:

```bash
kubectl get secret
```

### Configuring persistent volumes

Carbone requires persistent storage for templates. If multiple pods are used, render storage is also required.

These volumes must be configured as `ReadWriteMany`. You therefore need to choose the best implementation for your cloud provider (NFS is often the best solution).

Here is an example of yaml for the generic configuration of this volume:

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: carbone-storage
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Gi
```

To deploy it on the cluster :

```sh
kubectl apply -f carbone-volume.yaml
```

### Pods deployment

> ℹ️ **Note:** Here is an example of a yaml deployment with the creation of 3 Carbone pods. This `Deployment` does **not** enable template management or the job balancer — pods run independently, each with its own template storage under `subPath: template`, with no metadata replication between them. It is only suitable if you don't need clustering features; otherwise switch to a `StatefulSet` (or use the Helm chart, see above).

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: carbone-sample-deployment
  labels:
    name: carbone-ee
spec:
  replicas: 3
  selector:
    matchLabels:
      name: carbone-ee
  template:
    metadata:
      labels:
        name: carbone-ee
    spec:
      containers:
      - name: carbone
        image: "docker.io/carbone/carbone-ee:full"
        resources:
          requests:
            cpu: 1024m
            memory: 2048Mi
        ports:
        - containerPort: 4000
        env:
        - name: CARBONE_STUDIO
          value: "true"
        - name: CARBONE_LICENSE
          valueFrom:
            secretKeyRef:
              name: carbone-license
              key: license
        volumeMounts:
        - mountPath: /app/template
          name: carbone-storage
          subPath: template
        - mountPath: /app/render
          name: carbone-storage
          subPath: render
        livenessProbe:
          httpGet:
            path: /status          # The path to check for the liveness probe
            port: 4000             # The port to check on
          initialDelaySeconds: 15  # Wait this many seconds before starting the probe
          periodSeconds: 5         # Check the probe every 10 seconds
        readinessProbe:
          httpGet:
            path: /status          # The path to check for the readiness probe
            port: 4000             # The port to check on
          initialDelaySeconds: 5   # Wait this many seconds before starting the probe
          periodSeconds: 5         # Check the probe every 5 seconds
      volumes:
      - name: carbone-storage
        persistentVolumeClaim:
          claimName: carbone-storage
```

To deploy it on the cluster :

```sh
kubectl apply -f carbone-simple-deployment.yaml
```

You can check pods status :

```sh
kubectl get pod
```

### Service deployment

The final step is to declare the service to expose it:

```sh
kubectl expose deployment carbone-sample-deployment --type=LoadBalancer --name=carbone-service
```

You can check pods status :

```sh
kubectl get service
```

The API and Studio Carbone are now available:

```text
http://localhost:4000
```

### Authentication

To enable API authentication, you need to follow these steps :

**\- Set `CARBONE_AUTHENTICATION` to `true`**

**\- Generate private/public Carbone Key**

The key generation tool is included in the docker image from Carbone version 5 onwards.

```sh
docker run -it --platform "linux/amd64" carbone/carbone-ee:slim-5.0.0-beta.0 generate-keys
```

The two keys will be generated and displayed in the console.

Store content of public key in new kubernetes secret and map it to `/app/config/key.pub` on all Carbone pods. Create the file key.pem (with private key) and keep it secret on your side.

**\- Generate JWT token**

Follow interactive shell :

```sh
docker run -it --platform "linux/amd64" carbone/carbone-ee:slim-5.0.0-beta.0 generate-token

## Paste in terminal content of key.pem
```

A JWT token is then displayed in the console. You can then use it in your API calls.

## Troubleshooting

**Pod status and logs.**

```bash
kubectl get pods -n carbone
kubectl logs <pod-name> -n carbone
kubectl describe pod <pod-name> -n carbone
```

**Re-check the deployment summary.** `helm get notes carbone-ee -n carbone` re-prints the same warnings shown after install/upgrade (missing license, no persistent storage, no authentication...).

**PVC stuck in `Pending`.** The storage class backing `persistentVolumeClaimName` most likely doesn't support `ReadWriteMany` — check with `kubectl get storageclass` and confirm it can provision RWX volumes (see [Storage backends](#storage-backends) above).

**`ImagePullBackOff`.** Verify `image.repository` / `image.tag` in your `values.yaml`, and that the cluster can reach Docker Hub (or configure `imagePullSecrets` for a private registry mirror).

**Pods `CrashLoopBackOff` right after install.** Check `kubectl logs` for the actual error — the most common cause is a storage backend enabled in `values.yaml` (S3 or Azure Blob) with an incomplete configuration (missing bucket/container name or credentials).

## Related topics

- [Deploy with Docker](/documentation/developer/self-hosted-deployment/deploy-with-docker.md)
- [Deploy on Google Cloud Platform](/documentation/developer/self-hosted-deployment/deploy-on-gcp.md)
- [Deploy on Azure](/documentation/developer/self-hosted-deployment/deploy-on-azure.md)
- [Deploy on AWS](/documentation/developer/self-hosted-deployment/deploy-on-aws.md)
- [Carbone on Amazon Elastic Container Service](/documentation/developer/self-hosted-deployment/deploy-on-aws-ecs.md)
