Production Deployment & Configuration

GitHubEdit on GitHub

This page focuses on production deployment and configuration. For evaluation, see Quick Start.

Deployment Architecture & Components

Deployment architecture

Version Notes

  • v0.13.0 introduced collaboration-helper for snapshot and other CPU-intensive tasks.
  • v0.15.0 introduced tiered collaboration scheduling to route sheets by size.

Services

  • collaboration-server: Collaboration engine implementing OT on the server
  • collaboration-helper: CPU-intensive tasks like snapshot generation (v0.13.0+)
  • universer: Document operations and collaboration routing
  • exchange worker: Import/export worker (CPU and memory intensive)

collaboration-server is stateful. Edits for a document are routed to the same instance to improve performance and stability.

Dependencies

  • RDS: document metadata and permissions
  • Object Storage: document content, images, and blocks
  • Redis: collaborator cache and rate limiting
  • RabbitMQ: collaboration message broadcast
  • Temporal: import/export workflow engine

Deployment Options

OptionBest forNotes
Docker ComposeSmall scale, no K8sSingle-node, vertical scaling only
K8sMedium/large scaleHorizontal scaling and elasticity

Production Checklist

  • Identity & Permissions: do you need USIP integration?
  • Capacity planning: CPU/memory for collaboration and exchange
  • Infrastructure: prefer self-managed or cloud-managed components
  • Data safety: backup strategy for RDS and object storage
  • Observability: metrics and logs (see SRE Manual)

Configuration Entry Points

Docker Compose

Create .env.custom in the install directory to override defaults from .env.

K8s

Override Helm chart defaults via your own values.yaml (do not edit chart files directly).

Common Configurations

Enable USIP (Identity & Permissions)

.env.custom
# usip
USIP_ENABLED=true # set true to enable USIP
USIP_URI_CREDENTIAL=https://your-domain/usip/credential
USIP_URI_USERINFO=https://your-domain/usip/userinfo
USIP_URI_ROLE=https://your-domain/usip/role
USIP_URI_COLLABORATORS=https://your-domain/usip/collaborators
USIP_URI_UNITEDITTIME=https://your-domain/usip/unit-edit-time

# Apikey config is optinal.
USIP_APIKEY=

# auth
AUTH_PERMISSION_ENABLE_OBJ_INHERIT=false
AUTH_PERMISSION_CUSTOMER_STRATEGIES=
values.yaml
universer:
  config:
    usip:
      enabled: true
      uri:
        userinfo: 'https://your-domain/usip/userinfo'
        collaborators: 'https://your-domain/usip/collaborators'
        role: 'https://your-domain/usip/role'
        credential: 'https://your-domain/usip/credential'
        unitEditTime: 'https://your-domain/usip/unit-edit-time'

      # apikey is optional.
      apikey: ''
    auth:
      permission:
        enableObjInherit: false
        customerStrategies: ''

Field notes:

  • USIP_ENABLED / usip.enabled: enable USIP and configure all endpoints.
  • USIP_URI_*: endpoint URLs for credential, userinfo, role, collaborators, unit-edit-time.
  • AUTH_PERMISSION_*: permission point settings and object inheritance. See USIP.

Enable Event Sync

.env.custom
EVENT_SYNC=true # set true to enable
values.yaml
universer:
  config:
    rabbitmq:
      eventSync: true # set true to enable

Field notes:

  • Once enabled, you need a consumer to process events (see Event Sync).

Use Self-Managed Infrastructure

We recommend self-managed or cloud-managed components for better reliability and data safety.

Compatibility Requirements

  • MQ: AMQP/AMQPS compatible, RabbitMQ recommended. Built-in image: rabbitmq:3-management.
  • Redis: must support core commands (GET, MGET, SET, SETNX, DEL, EXISTS, EXPIRE, HSET, HGET, HDEL, HGETALL, HLEN, SCAN, Pipeline, EVAL, EVALSHA).
  • RDS: PostgreSQL 16.1, MySQL 8.0, GaussDB, DamengDB. Distributed databases requiring sharding are not supported; TiDB and similar are compatible but may have hotspot risks due to monotonic IDs.
  • Object Storage: AWS S3 compatible. Built-in image: bitnami/minio:2024.8.3-debian-12-r1.

RDS

Note: Temporal does not support GaussDB or DamengDB; Temporal will use a separate PostgreSQL.

PostgreSQL-compatible
.env.custom
DISABLE_UNIVER_RDS=true

DATABASE_DRIVER=postgresql
DATABASE_HOST=your-database-host
DATABASE_PORT=your-database-port
DATABASE_DBNAME=univer
DATABASE_USERNAME=user-name
DATABASE_PASSWORD=password
values.yaml
postgresql:
  enabled: false

universer:
  config:
    database:
      driver: postgresql
      host: your-database-host
      port: your-database-port
      dbname: univer
      username: postgres
      password: postgres

temporal:
  server:
    config:
      persistence:
        default:
          driver: sql
          sql:
            driver: postgres12
            host: your-database-host
            port: your-database-port
            database: temporal
            user: postgres
            password: postgres

        visibility:
          driver: sql
          sql:
            driver: postgres12
            host: your-database-host
            port: your-database-port
            database: temporal_visibility
            user: postgres
            password: postgres

Field notes:

  • DISABLE_UNIVER_RDS: disable built-in database when using your own RDS.
  • DATABASE_DBNAME must match the database name in the init scripts.
  • DATABASE_USERNAME should have select/insert/update/delete privileges.
  • Temporal databases are only used for import/export workflows, not document data.
  • driver: postgres12 requires PostgreSQL 12+.
MySQL-compatible
.env.custom
DISABLE_UNIVER_RDS=true

DATABASE_DRIVER=mysql
DATABASE_HOST=your-database-host
DATABASE_PORT=your-database-port
DATABASE_DBNAME=univer
DATABASE_USERNAME=user-name
DATABASE_PASSWORD=password
values.yaml
postgresql:
  enabled: false

universer:
  config:
    database:
      driver: mysql
      host: your-database-host
      port: your-database-port
      dbname: univer
      username: mysql
      password: mysql

temporal:
  server:
    config:
      persistence:
        default:
          driver: sql
          sql:
            driver: mysql8
            host: your-database-host
            port: your-database-port
            database: temporal
            user: mysql
            password: mysql

        visibility:
          driver: sql
          sql:
            driver: mysql8
            host: your-database-host
            port: your-database-port
            database: temporal_visibility
            user: mysql
            password: mysql

Field notes:

  • DATABASE_DRIVER=mysql maps to MySQL 8.x.
  • driver: mysql8 requires MySQL 8.x.
  • Other fields match the PostgreSQL setup.
GaussDB
.env.custom
DISABLE_UNIVER_RDS=true

DATABASE_DRIVER=gaussdb
DATABASE_HOST=your-database-host
DATABASE_PORT=your-database-port
DATABASE_DBNAME=univer
DATABASE_USERNAME=user-name
DATABASE_PASSWORD=password
values.yaml
universer:
  config:
    database:
      driver: gaussdb
      host: your-database-host
      port: your-database-port
      dbname: univer
      username: gaussdb
      password: gaussdb

Field notes:

  • GaussDB is not supported by Temporal, so no Temporal config is needed.
  • Other fields match the PostgreSQL setup.
DamengDB
.env.custom
DISABLE_UNIVER_RDS=true

DATABASE_DRIVER=dameng
DATABASE_HOST=your-database-host
DATABASE_PORT=your-database-port
DATABASE_DBNAME=univer
DATABASE_USERNAME=user-name
DATABASE_PASSWORD=password
values.yaml
universer:
  config:
    database:
      driver: dameng
      host: your-database-host
      port: your-database-port
      dbname: univer
      username: dameng
      password: dameng

Field notes:

  • DamengDB is not supported by Temporal, so no Temporal config is needed.
  • Other fields match the PostgreSQL setup.

Redis

.env.custom
DISABLE_UNIVER_REDIS=true

REDIS_ADDR=host:port[,host:port]
REDIS_USERNAME=user-name
REDIS_PASSWORD=password
REDIS_DB=0

REDIS_TLS_ENABLED=false
REDIS_TLS_INSECURE=false
REDIS_TLS_CA=
REDIS_TLS_CERT=
REDIS_TLS_KEY=
values.yaml
universer:
  config:
    redis:
      poolSize: 100
      addr: 192.168.1.100:6379
      read_timeout: 1s
      write_timeout: 1s
      db: 0
      username: user_name_here
      password: password_here
      tlsConfig:
        enabled: false
        insecure: false
        ca: ''
        cert: ''
        key: ''

worker:
  redis:
    poolSize: 10
    addr: 192.168.1.100:6379
    read_timeout: 1s
    write_timeout: 1s
    db: 0
    username: user_name_here
    password: password_here
    tlsConfig:
      enabled: false
      insecure: false
      ca: ''
      cert: ''
      key: ''

redis:
  enabled: false

Field notes:

  • DISABLE_UNIVER_REDIS: disable built-in Redis when using your own.
  • REDIS_ADDR: comma-separated list for Redis cluster.
  • TLS modes: insecure, CA-only, or mTLS.
  • For Docker Compose, CA/cert/key can be file paths or content; if paths, use container-mounted paths.
  • For K8s, tlsConfig typically uses inline content.
  • worker.redis should match universer.config.redis.

MQ

.env.custom
DISABLE_UNIVER_MQ=true

RABBITMQ_CLUSTER_ENABLED=true
RABBITMQ_CLUSTER_USERNAME=user-name
RABBITMQ_CLUSTER_PASSWORD=password
RABBITMQ_CLUSTER_ADDR=host:port[,host:port]
RABBITMQ_CLUSTER_VHOST=/
RABBITMQ_CLUSTER_SCHEMA=amqp
values.yaml
universer:
  config:
    rabbitmq:
      cluster:
        enabled: true
        addr: '192.168.1.2:5672,192.168.1.5:5672'
        username: user-here
        password: password-here
        vhost: /
        schema: amqp

rabbitmq:
  enabled: false

Field notes:

  • DISABLE_UNIVER_MQ: disable built-in RabbitMQ when using your own.
  • RABBITMQ_CLUSTER_ENABLED must be true.
  • RABBITMQ_CLUSTER_ADDR supports multiple addresses; each must be readable and writable.
  • RABBITMQ_CLUSTER_VHOST defaults to /.
  • RABBITMQ_CLUSTER_SCHEMA is amqp or amqps.

Object Storage

.env.custom
DISABLE_UNIVER_S3=true

S3_USER=user
S3_PASSWORD=password
S3_REGION=your-inner-s3like-region
S3_PATH_STYLE=true|false
S3_ENDPOINT=inner-visit-host:port
S3_ENDPOINT_PUBLIC=public-visit-host:port
S3_DEFAULT_BUCKET=default-bucket-name
values.yaml
universer:
  config:
    s3:
      accessKeyID: admin
      accessKeySecret: minioadmin
      region: us-east-1
      endpoint: http://192.168.1.100:9000
      endpointPublic: http://192.168.1.100:9001
      usePathStyle: true
      defaultBucket: univer

minio:
  enabled: false

Field notes:

  • DISABLE_UNIVER_S3: disable built-in MinIO when using your own.
  • S3_PATH_STYLE: true for Path-Style, false for Virtual-Host Style.
  • S3_ENDPOINT: internal endpoint for backend services.
  • S3_ENDPOINT_PUBLIC: public endpoint for client downloads.
  • S3_DEFAULT_BUCKET: default bucket name.

Enable Observability

.env.custom
ENABLE_UNIVER_OBSERVABILITY=true
GRAFANA_USERNAME=set-your-admin-user-name-here
GRAFANA_PASSWORD=set-your-admin-user-password-here
HOST_GRAFANA_PORT=set-the-port-you-want

For K8s, install the observability stack separately. See SRE Manual.

Field notes:

  • ENABLE_UNIVER_OBSERVABILITY=true enables built-in Grafana/Prometheus.
  • HOST_GRAFANA_PORT is the external Grafana port.

Capacity & Scaling

.env.custom
UNIVERSER_REPLICATION_CNT=2
COLLABORATION_SERVER_REPLICATION_CNT=2
COLLABORATION_SERVER_MEMORY_LIMIT=2048
COLLABORATION_HELPER_REPLICATION_CNT=2
COLLABORATION_HELPER_MEMORY_LIMIT=2048

EXCHANGE_WORKER_REPLICATION_CNT=1
EXCHANGE_WORKER_MEMORY_LIMIT=4096
EXCHANGE_WORKER_IMPORT_CONCURRENT=1
EXCHANGE_WORKER_EXPORT_CONCURRENT=1
values.yaml
universer:
  replicaCount: 3
collaboration-server:
  replicaCount: 3
  maxMemoryLimit: 2048
collaboration-helper-server:
  replicaCount: 2
worker:
  replicaCount: 1
  temporalWorker:
    importConcurrent: 1
    exportConcurrent: 1

Field notes:

  • *_REPLICATION_CNT / replicaCount: number of instances.
  • *_MEMORY_LIMIT / maxMemoryLimit: memory limit in MB.
  • EXCHANGE_WORKER_*_CONCURRENT: concurrency per worker.

Tiered Collaboration Scheduling (Advanced)

v0.15.0 adds tiered scheduling to route documents by size to different collaboration clusters. It is disabled by default.

Example:

unitRoutingConf:
  tiers:
    - tierName: normal
      enable: true
      upgradeCellsThreshold: 0
      downgradeCellsThreshold: 0
      upgradeImportTimeThreshold: 0
    - tierName: large
      enable: true
      upgradeCellsThreshold: 1000
      downgradeCellsThreshold: 900
      upgradeImportTimeThreshold: 10
    - tierName: huge
      enable: true
      upgradeCellsThreshold: 10000
      downgradeCellsThreshold: 9000
      upgradeImportTimeThreshold: 60

Field notes:

  • tiers: list of cluster tiers.
  • enable: enable or ignore a tier.
  • upgradeCellsThreshold: min cells to upgrade to the tier.
  • downgradeCellsThreshold: downgrade threshold to avoid flapping.
  • upgradeImportTimeThreshold: import-time threshold for new sheets.
  • At least one tier must have all thresholds set to 0.

Scheduling example:

  • A new workbook with 0 cells goes to normal.
  • At 1000 cells, it upgrades to large.
  • If it drops to 899 cells, it downgrades to normal.
  • A workbook imported in 61 seconds goes to huge.

Docker Compose ships with two tiers (normal/large):

.env.custom
ENABLE_LARGE_TIER_COLLABORATION_SERVER=false
LARGE_TIER_MIN_CELLS_COUNT=2000000
LARGE_TIER_DOWNGRADE_CELLS_COUNT=1950000
LARGE_TIER_MIN_IMPORT_SECONDS=10
LARGE_TIER_COLLABORATION_SERVER_REPLICATION_CNT=2
LARGE_TIER_COLLABORATION_SERVER_MEMORY_LIMIT=8192

Configure multi-tier clusters and routing rules yourself.

Network & CORS

.env.custom
DOCKER_NETWORK_SUBNET=172.30.0.0/16
HOST_NGINX_PORT=the-univer-server-api-port-you-wanted
HOST_MINIO_PORT=the-minio-port-you-wanted
HOST_GRAFANA_PORT=the-grafana-port-you-wanted

CORS_ALLOW_ORIGINS='["domain1", "domain2"]'
CORS_ALLOW_HEADERS='["content-type","authorization"]'
values.yaml
universer:
  config:
    server:
      http:
        corsAllowOrigins: [domain1, domain2]
        corsAllowHeaders: [content-type, authorization]

Field notes:

  • DOCKER_NETWORK_SUBNET: Docker network CIDR.
  • HOST_NGINX_PORT: external API port; change if it conflicts.
  • HOST_MINIO_PORT / HOST_GRAFANA_PORT only apply to built-in components.
  • K8s does not need these port mappings.
  • CORS_ALLOW_ORIGINS / CORS_ALLOW_HEADERS control allowed origins and headers.

K8s Production Required

The Helm chart deploys a demo UI by default and uses its domain. For production, disable the demo and set your domain:

values.yaml
collaboration-demo:
  enabled: false
universer:
  ingress:
    enabled: true
    hosts:
      - host: use-your-own-domain-here
        paths:
          - path: /universer-api/
            pathType: Prefix

Configuration Example

This example enables USIP and event sync, uses self-managed RDS/object storage, and adjusts scaling:

.env.custom
USIP_ENABLED=true
USIP_URI_CREDENTIAL=https://usip-demo.univer.ai/usip/credential
USIP_URI_USERINFO=https://usip-demo.univer.ai/usip/userinfo
USIP_URI_ROLE=https://usip-demo.univer.ai/usip/role
USIP_URI_COLLABORATORS=https://usip-demo.univer.ai/usip/collaborators
USIP_URI_UNITEDITTIME=https://usip-demo.univer.ai/unit-edit-time

AUTH_PERMISSION_CUSTOMER_STRATEGIES=[ {"action": 3, "role": 2}, {"action": 6, "role": 2} ]

EVENT_SYNC=true

DISABLE_UNIVER_RDS=true
DATABASE_DRIVER=postgresql
DATABASE_HOST=univer-postgresql
DATABASE_PORT=5432
DATABASE_DBNAME=univer
DATABASE_USERNAME=universer-biz
DATABASE_PASSWORD=123456

DISABLE_UNIVER_S3=true
S3_USER=universer-biz
S3_PASSWORD=123456
S3_REGION=cn-sz
S3_PATH_STYLE=true
S3_ENDPOINT=univer-s3:9652
S3_ENDPOINT_PUBLIC=univer.ai:9653
S3_DEFAULT_BUCKET=univer

HOST_NGINX_PORT=8010

UNIVERSER_REPLICATION_CNT=4
COLLABORATION_SERVER_REPLICATION_CNT=5
COLLABORATION_SERVER_MEMORY_LIMIT=2048
values.yaml
collaboration-demo:
  enabled: false

postgresql:
  enabled: false

minio:
  enabled: false

collaboration-server:
  replicaCount: 5
  maxMemoryLimit: 2048

universer:
  replicaCount: 4
  ingress:
    enabled: true
    hosts:
      - host: usip-demo.univer.ai
        paths:
          - path: /universer-api/
            pathType: Prefix
  config:
    usip:
      enabled: true
      uri:
        userinfo: 'https://usip-demo.univer.ai/usip/userinfo'
        collaborators: 'https://usip-demo.univer.ai/usip/collaborators'
        role: 'https://usip-demo.univer.ai/usip/role'
        credential: 'https://usip-demo.univer.ai/usip/credential'
        unitEditTime: 'https://usip-demo.univer.ai/unit-edit-time'
    auth:
      permission:
        customerStrategies: '[ {"action": 3, "role": 2}, {"action": 6, "role": 2} ]'
    rabbitmq:
      eventSync: true
    database:
      driver: postgresql
      host: univer-postgresql
      port: 5432
      dbname: univer
      username: universer-biz
      password: 123456
    s3:
      accessKeyID: universer-biz
      accessKeySecret: 123456
      region: cn-sz
      endpoint: 'univer-s3:9652'
      endpointPublic: 'univer.ai:9653'
      usePathStyle: true
      defaultBucket: univer

temporal:
  server:
    config:
      persistence:
        default:
          driver: sql
          sql:
            driver: postgres12
            host: univer-postgresql
            port: 5432
            database: temporal
            user: universer-temporal
            password: 123456

        visibility:
          driver: sql
          sql:
            driver: postgres12
            host: univer-postgresql
            port: 5432
            database: temporal_visibility
            user: universer-temporal
            password: 123456

Deployment Steps

Docker Compose

  1. Prepare .env.custom (optional)
  2. Prepare license.txt and licenseKey.txt
  3. If using self-managed RDS, download the DB init scripts and initialize
  4. Get Univer Server
    • Online: bash -c "$(curl -fsSL https://get.univer.ai/product)" [-- version]
    • Offline: download the All-in-one package and run bash load-images.sh
  5. Place .env.custom in the server root
  6. Copy license files into configs/
  7. Run bash run.sh start in the server root
  8. Run regression tests

K8s

  1. Prepare values.yaml (optional, version-controlled recommended)

  2. Prepare license.txt and licenseKey.txt

  3. If using self-managed RDS, run the init scripts

  4. Install via Helm

    • Online:
    helm install -n univer --create-namespace \
      -f your-own-values.yaml-path \
      --set global.istioNamespace="univer" \
      --set-file universer.license.licenseV2=your-license.txt-path \
      --set-file universer.license.licenseKeyV2=your-licenseKey.txt-path \
      univer-stack \
      oci://univer-acr-registry.cn-shenzhen.cr.aliyuncs.com/helm-charts/univer-stack \
      --version target-version
    
    kubectl rollout restart -n univer deployment/collaboration-server
    kubectl rollout restart -n univer deployment/universer
    • Offline:
    export REGISTER=XXXX
    export NAMESPACE=XXX
    docker login $REGISTER
    bash load-image.sh --registry $REGISTER --namespace $NAMESPACE
    
    helm install -n univer --create-namespace \
      -f your-own-values.yaml-path \
      --set global.istioNamespace="univer" \
      --set-file universer.license.licenseV2=your-license.txt-path \
      --set-file universer.license.licenseKeyV2=your-licenseKey.txt-path \
      univer-stack-xxxx.tgz

Parameter notes:

  • -n univer: namespace (keep consistent for upgrades).
  • univer-stack: Helm release name (keep consistent for upgrades).
  • --version target-version: target version; omit for latest.
  • -f your-own-values.yaml-path: path to your custom values file.
  • --set-file universer.license.licenseV2: path to license.txt.
  • --set-file universer.license.licenseKeyV2: path to licenseKey.txt.

To install built-in observability:

helm upgrade --install -n univer-observability --create-namespace \
  --set global.univerNamespace="univer" \
  univer-observability \
  oci://univer-acr-registry.cn-shenzhen.cr.aliyuncs.com/helm-charts/univer-observability

How is this guide?