Self-hosted deployment

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.

The recommended way to deploy Carbone on Kubernetes is our official Helm chart, available on Artifact Hub. 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.

By default, the program runs with free Community features. To use Carbone Enterprise Edition, you need a Carbone license. Contact us to learn more, or request a free 30-day trial in our chat.

Quickstart with Helm

Prerequisites

Installation

Add the Carbone Helm repository:

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

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

applicationConfiguration:
  license: "<YOUR_CARBONE_LICENSE>"

Install the chart:

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

Then check that everything is running correctly:

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

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

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

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).

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 — 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:

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.

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

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

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

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

To uninstall:

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.

The example below uses a plain Deployment, which only supports simple, stateless setups. If you need template management or the 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 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 :

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:

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:

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

To deploy it on the cluster :

kubectl apply -f carbone-volume.yaml

Pods deployment

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).

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 :

kubectl apply -f carbone-simple-deployment.yaml

You can check pods status :

kubectl get pod

Service deployment

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

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

You can check pods status :

kubectl get service

The API and Studio Carbone are now available:

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.

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 :

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.

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 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).