Skip to content

SiYuan

Official upstream repository.

Private knowledge workspace with a persistent filesystem, native access-code authentication and optional OIDC. The chart uses the official docker.io/b3log/siyuan:v3.8.3 image, verified for Linux amd64, arm64 and arm.

Production contract

  • Exactly one writer per workspace, enforced even for RWX claims. Deployment strategy is Recreate.
  • Complete workspace persistence, including documents, configuration, history, assets and encrypted content.
  • Native access code in a Kubernetes Secret, generated once and retained on Helm upgrades.
  • Existing Secret and External Secrets Operator support for GitOps credentials.
  • Optional native OIDC with explicit claim admission rules and Secret-backed client credentials.
  • Non-root UID/GID 1000, read-only image filesystem, RuntimeDefault seccomp, dropped capabilities and no API token.
  • Native boot-completion readiness, resource defaults, dual-stack Service, Ingress, Gateway API and explicit egress policies.
  • Functional notebook/document tests, pod-replacement persistence checks and quiesced restore into a fresh PVC.

Install

helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install siyuan helmforge/siyuan --namespace siyuan --create-namespace
kubectl -n siyuan port-forward service/siyuan-siyuan 6806:6806

Open http://localhost:6806 and use the access code described in Helm NOTES. For remote access, configure an HTTPS hostname and a WebSocket-capable ingress or Gateway. SiYuan is a personal administrative workspace; this is not a tenant-isolated collaboration server.

Storage and availability

The default claim is 10Gi ReadWriteOnce and mounts at /siyuan/workspace. The chart keeps the generated PVC on uninstall. Existing claims are never created or deleted by the chart. Reusing a retained claim requires persistence.existingClaim.

persistence:
  existingClaim: restored-workspace

There is no external PostgreSQL/MySQL/Redis backend. Do not scale replicas or share the workspace between independent releases. RWX does not make multiple writers safe. Recreate deliberately permits downtime during upgrades so the old process exits before the new writer starts. No HPA or PDB is provided for this singleton.

persistence.enabled: false is for disposable trials; replacement loses workspace data. A StorageClass of - requests no dynamic provisioning. Provision a compatible PV yourself in that case. fsGroupChangePolicy OnRootMismatch avoids repeated ownership walks where supported by the CSI driver. The chart does not run a privileged recursive chown init container.

Authentication

The native access code is read from SIYUAN_ACCESS_AUTH_CODE. It never appears in container arguments or ConfigMaps. Empty auth.accessCode generates a random 32-character code and retains it via Helm lookup. Existing-secret mode bypasses generated credentials.

auth:
  existingSecret: siyuan-auth
  accessCodeKey: access-code

Use a Secret containing that key and avoid committing plaintext credentials. Changing an existing Secret requires a pod restart or your configured Secret-reload controller. Generated inline values change the checksum and roll the pod. Preserve the Secret for disaster recovery. The API token and the access code are different native credentials.

Local access-code login remains available when OIDC is enabled. Treat it as an administrative recovery credential and restrict access to its Secret. OIDC admission grants administration of the same workspace; it does not create isolated per-user workspaces.

Native OIDC

oidc:
  enabled: true
  provider: custom
  issuerURL: https://identity.example.com/realms/personal
  clientID: siyuan
  existingSecret: siyuan-oidc
  clientSecretKey: client-secret
  redirectURL: https://notes.example.com/api/system/oidc/callback
  claimRules:
    - claim: email
      operator: equals
      values:
        - owner@example.com
    - claim: email_verified
      operator: equals
      values:
        - 'true'

Register the exact HTTPS callback in your provider. Remote HTTP callbacks are rejected upstream. Configure DNS and HTTPS egress to the issuer’s discovery, JWKS and token endpoints. Use the standard custom OIDC provider contract; provider-specific OAuth adapters are not exposed by this chart.

Rules are ANDed, with OR across values within each rule. Claims are top-level keys. equals is case-sensitive; contains performs substring matching and is unsuitable for many exact identity restrictions. Boolean claims are converted to strings. Empty rules are rejected unless allowAll explicitly grants the workspace to every authenticated identity. Keep allowAll false.

The runtime fixture exercises real discovery, JWKS/RS256 validation, authorization code, PKCE, nonce and rejected identity claims. Its instant-login Node provider is disposable test infrastructure and must never be deployed as an identity service.

External Secrets

auth:
  existingSecret: siyuan-auth
externalSecrets:
  enabled: true
  items:
    - fullnameOverride: siyuan-auth
      spec:
        secretStoreRef:
          name: production
          kind: ClusterSecretStore
        data:
          - secretKey: access-code
            remoteRef:
              key: applications/siyuan/access-code

Install External Secrets Operator and the store first. The chart renders the v1 API and requires a store or per-item source reference. The target Secret must match auth.existingSecret or oidc.existingSecret. Native login in the ESO CI profile verifies actual consumption after Ready=True.

Network and exposure

The Service defaults to ClusterIP:6806. Use a dedicated root hostname; configure long-lived WebSocket support at the edge. Gateway API requires existing v1 CRDs and a Gateway controller. TLS configuration belongs to the edge; the native kernel’s local certificates are retained in workspace configuration.

Default NetworkPolicy permits HTTP from pods in the release namespace and DNS egress. It denies other outbound traffic. Set ingressFrom for the edge namespace, and add explicit webEgress/extraEgress for OIDC, cloud sync, webhooks or extension downloads. Permit all endpoints involved in your provider or sync service. Policy enforcement requires a capable CNI.

PreferDualStack can fall back to single-stack. RequireDualStack requires cluster support. Extra containers inherit the pod identity but need their own restrictive security context and resource limits. They are an integration extension, not an alternative application architecture.

Health and resources

Startup and readiness inspect native boot progress and require 100 percent completion. Liveness uses the unauthenticated version endpoint so an external sync outage does not restart a healthy workspace. Initial requests are 100m CPU and 256Mi memory, with 1 CPU/1Gi limits. Size for indexing, document count and plugins. A large workspace may require a longer startup budget and more memory.

No native Prometheus endpoint is exposed by this release. The chart does not fabricate a ServiceMonitor. Monitor workload/PVC metrics and edge availability through your existing observability stack; use authenticated behavioral checks for deeper application coverage.

Backup and recovery

Back up the complete workspace after gracefully stopping the writer, or use an application-consistent storage procedure appropriate to your platform. Copying a live SQLite file alone is unsafe and omits documents, assets and encryption state. Protect backups as sensitive: configuration includes identity and cryptographic material.

The recovery CI profile scales the writer to zero, archives the complete workspace, restores it into a fresh claim, then starts the chart using existingClaim. It verifies native login and an identical exported Unicode document. See recovery procedure.

Encrypted notebooks also require their recovery passwords and native encrypted backups. Kubernetes storage alone cannot reconstruct a lost encryption password. Keep recovery credentials separately protected and exercise restore regularly.

Upgrade

helm upgrade siyuan helmforge/siyuan -n siyuan -f production.yaml
kubectl -n siyuan rollout status deployment/siyuan-siyuan

Take a quiesced backup first. Review upstream storage-format changes and allow Recreate downtime. Helm rollback changes manifests and image, not data migrations. Restore a compatible workspace backup when an older version cannot read newer storage.

Security Scan

Security Scan: siyuan

Framework Score
MITRE + NSA + SOC2 100%

Security posture acceptable.

Measured with Kubescape 4.0.13 against default manifests. This assesses deployment configuration, not every upstream feature or extension.

Values

See values.yaml and values.schema.json for the full contract, including scheduling and dual-stack settings.

Workspace recovery

Consistent backup

Schedule downtime, stop the Deployment and wait for the old pod to terminate. Use the same namespace and explicit context for all commands. Do not run a second SiYuan process against the claim.

kubectl -n siyuan scale deployment/siyuan-siyuan --replicas=0
kubectl -n siyuan wait --for=delete pod -l app.kubernetes.io/instance=siyuan --timeout=60s

Mount the workspace read-only in a temporary restricted maintenance pod, along with a separate writable backup volume. Use the pinned SiYuan image’s tar utility to archive the entire workspace. Store each backup under a unique name. Preserve file permissions and the full data/conf/history/storage tree, including encrypted content and identity material.

After a successful archive and integrity check, remove the maintenance pod and scale the original Deployment back to one. Replicas remain one in Helm values so a subsequent upgrade does not preserve maintenance downtime accidentally.

Restore

Provision a new empty claim of sufficient capacity. With no application writer using it, mount the archive volume and extract the entire workspace into the new claim using UID/GID 1000. Remove the maintenance pod before starting the application.

persistence:
  existingClaim: siyuan-recovered

Install the same tested image version with this value and the original auth/OIDC Secret references. Verify native login, notebook listing, document export, attachments and any encrypted notebooks before routing traffic. The chart’s recovery CI follows this procedure with a quiesced archive and a fresh claim, and verifies identical Unicode document content.

Do not substitute a copy of the live SQLite index for a workspace backup. Do not expect Helm rollback to undo storage migrations. Keep recovery passwords for encrypted notebooks separately; also create native encrypted-notebook backups where appropriate.

Retention

The generated PVC is kept on release uninstall by default. Uninstalling is not a backup. Namespace deletion, underlying-volume deletion and storage-provider policies are separate operations. Configure backup encryption, off-cluster copies and retention according to your data requirements.

Complete values

# SPDX-License-Identifier: Apache-2.0
# -- Override the chart name used in resource names.
nameOverride: ''
# -- Override the complete resource name.
fullnameOverride: ''
# -- Extra resource labels; selector labels are reserved.
commonLabels: {}
# -- Exactly one writer per workspace, including RWX volumes.
replicaCount: 1
# -- Image.
image:
  # -- Repository.
  repository: docker.io/b3log/siyuan
  # -- Verified stable image tag.
  tag: v3.8.3
  # -- Kubernetes pull policy.
  pullPolicy: IfNotPresent
# -- Registry credentials for a private mirror.
imagePullSecrets: []
# -- Auth.
auth:
  # -- Native administrative access code. Empty generates and retains 32 random alphanumeric characters.
  accessCode: ''
  # -- Existing access-code Secret; do not combine with an inline code.
  existingSecret: ''
  # -- Key containing the native access code, distinct from an API token.
  accessCodeKey: access-code
# -- Native kernel HTTP listener.
server:
  # -- Container listen port; Service and probes follow this value.
  port: 6806
# -- Additional environment entries; chart-managed security variables cannot be overridden.
extraEnv: []
# -- Additional envFrom Secret/ConfigMap references.
envFrom: []
# -- Service Account.
serviceAccount:
  # -- Create a dedicated ServiceAccount with no API permissions.
  create: true
  # -- Existing or overridden ServiceAccount name.
  name: ''
  # -- ServiceAccount annotations.
  annotations: {}
  # -- Automount Service Account Token.
  automountServiceAccountToken: false
# -- Service.
service:
  # -- Kubernetes Service type.
  type: ClusterIP
  # -- Service HTTP port.
  port: 6806
  # -- Service annotations.
  annotations: {}
  # -- Service IP family policy; empty uses cluster default.
  ipFamilyPolicy: ''
  # -- Requested address families; RequireDualStack needs a dual-stack cluster.
  ipFamilies: []
# -- Ingress.
ingress:
  # -- Enable Ingress. TLS is configured through ingress.tls.
  enabled: false
  # -- Ingress controller class; empty omits the field.
  ingressClassName: ''
  # -- Annotations.
  annotations: {}
  # -- Host/path rules; at least one explicit host is required when enabled.
  hosts: []
  # -- TLS host/Secret entries.
  tls: []
# -- Gateway.
gatewayAPI:
  # -- Render canonical Gateway API HTTPRoutes.
  enabled: false
  # -- Route definitions with parentRefs, hostnames, rules, labels and annotations.
  httpRoutes: []
# -- External Secrets.
externalSecrets:
  # -- Enabled.
  enabled: false
  # -- Default operator refresh interval.
  refreshInterval: 1h
  # -- Items.
  items: []
# -- Network Policy.
networkPolicy:
  # -- Enabled.
  enabled: true
  # -- Allowed ingress peers. Empty permits pods in the same namespace only.
  ingressFrom: []
  # -- Enable outbound isolation, allowing DNS and configured web ports.
  egressIsolation: true
  # -- DNS peers; defaults to cluster pods in any namespace, restricted to DNS ports.
  dnsEgress:
    - namespaceSelector: {}
  # -- Explicit web destinations for OIDC, sync or plugins; empty denies web egress.
  webEgress: []
  # -- TCP ports allowed to the declared web destinations.
  webPorts:
    - 443
  # -- Additional egress rules, for example internal APIs on alternate ports.
  extraEgress: []
# -- Probes.
probes:
  # -- Wait for native kernel boot completion before liveness starts.
  startup:
    # -- Enabled.
    enabled: true
    # -- Path.
    path: /api/system/bootProgress
    # -- Require native boot progress 100 before startup/readiness succeeds.
    requireBootComplete: true
    # -- Period Seconds.
    periodSeconds: 5
    # -- Timeout Seconds.
    timeoutSeconds: 2
    # -- Failure Threshold.
    failureThreshold: 60
  # -- Check local HTTP availability without requiring external services.
  liveness:
    # -- Enabled.
    enabled: true
    # -- Path.
    path: /api/system/version
    # -- Period Seconds.
    periodSeconds: 20
    # -- Timeout Seconds.
    timeoutSeconds: 3
    # -- Failure Threshold.
    failureThreshold: 3
  # -- Require complete kernel initialization before admitting traffic.
  readiness:
    # -- Enabled.
    enabled: true
    # -- Path.
    path: /api/system/bootProgress
    # -- Require native boot progress 100 before startup/readiness succeeds.
    requireBootComplete: true
    # -- Period Seconds.
    periodSeconds: 10
    # -- Timeout Seconds.
    timeoutSeconds: 3
    # -- Failure Threshold.
    failureThreshold: 3
# -- Resource sizing for indexing and workspace operations; increase memory for large collections.
resources:
  # -- Requests.
  requests:
    # -- Cpu.
    cpu: 100m
    # -- Memory.
    memory: 256Mi
  # -- Limits.
  limits:
    # -- Cpu.
    cpu: '1'
    # -- Memory.
    memory: 1Gi
# -- Non-root pod identity and CSI-assisted ownership for the workspace.
podSecurityContext:
  # -- Run As Non Root.
  runAsNonRoot: true
  # -- Run As User.
  runAsUser: 1000
  # -- Run As Group.
  runAsGroup: 1000
  # -- Fs Group.
  fsGroup: 1000
  # -- Fs Group Change Policy.
  fsGroupChangePolicy: OnRootMismatch
  # -- Seccomp Profile.
  seccompProfile:
    # -- Type.
    type: RuntimeDefault
# -- Restricted container privileges; all configuration and assets are read-only.
securityContext:
  # -- Allow Privilege Escalation.
  allowPrivilegeEscalation: false
  # -- Read Only Root Filesystem.
  readOnlyRootFilesystem: true
  # -- Capabilities.
  capabilities:
    # -- Drop.
    drop:
      - ALL
# -- Pod labels; immutable selector labels cannot be overridden.
podLabels: {}
# -- Pod annotations, e.g. for an external Secret reloader.
podAnnotations: {}
# -- Node selection constraints.
nodeSelector: {}
# -- Scheduling tolerations.
tolerations: []
# -- Pod affinity or anti-affinity.
affinity: {}
# -- Topology spreading across nodes or zones.
topologySpreadConstraints: []
# -- Scheduling priority class.
priorityClassName: ''
# -- Grace period for HTTP shutdown.
terminationGracePeriodSeconds: 30
# -- Complete workspace storage, including configuration, data, history and encrypted content.
persistence:
  # -- Persist the complete workspace. Disable only for disposable tests.
  enabled: true
  # -- Pre-existing workspace PVC, never created or deleted by this chart.
  existingClaim: ''
  # -- StorageClass name; empty uses cluster default, dash disables dynamic provisioning.
  storageClass: ''
  # -- Workspace claim capacity.
  size: 10Gi
  # -- PVC access mode. Even ReadWriteMany must have exactly one application writer.
  accessModes:
    - ReadWriteOnce
  # -- Keep the generated PVC on Helm uninstall; this does not protect against namespace deletion.
  retain: true
  # -- Annotations.
  annotations: {}

# -- Native OIDC admission to this single administrative workspace. Local access-code login remains available.
oidc:
  # -- Enable native OIDC alongside the local administrative access-code login.
  enabled: false
  # -- Standard custom OIDC provider contract. Provider-specific OAuth adapters are not exposed.
  provider: custom
  # -- Issuer URL used for discovery, JWKS and token exchange. Permit its destinations in egress policy.
  issuerURL: ''
  # -- Registered OIDC client identifier.
  clientID: ''
  # -- Inline client credential stored only in Secret; prefer existingSecret for GitOps.
  clientSecret: ''
  # -- Existing Secret containing the client credential; cannot be combined with inline clientSecret.
  existingSecret: ''
  # -- Key containing the OIDC client credential.
  clientSecretKey: client-secret
  # -- Scopes requested during authorization; include openid.
  scopes:
    - openid
    - profile
    - email
  # -- Exact HTTPS callback ending in /api/system/oidc/callback. Register the same URL in the provider.
  redirectURL: ''
  # -- Grant workspace administration to every authenticated identity. Keep false and configure claimRules.
  allowAll: false
  # -- AND across rules, OR across values. Use equals for exact top-level claim admission.
  claimRules: []
# -- Optional companion containers; set their security contexts, probes and resources explicitly.
extraContainers: []

Production example

# SPDX-License-Identifier: Apache-2.0
auth:
  existingSecret: siyuan-auth
persistence:
  size: 20Gi
ingress:
  enabled: true
  ingressClassName: traefik
  hosts:
    - host: notes.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: siyuan-tls
      hosts:
        - notes.example.com
networkPolicy:
  ingressFrom:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-system

Oidc example

# SPDX-License-Identifier: Apache-2.0
# Replace documentation-only 192.0.2.0/24 with your identity-provider destinations.
auth:
  existingSecret: siyuan-auth
oidc:
  enabled: true
  issuerURL: https://identity.example.com/realms/personal
  clientID: siyuan
  existingSecret: siyuan-oidc
  redirectURL: https://notes.example.com/api/system/oidc/callback
  claimRules:
    - claim: email
      operator: equals
      values:
        - owner@example.com
    - claim: email_verified
      operator: equals
      values:
        - 'true'
networkPolicy:
  webEgress:
    - ipBlock:
        cidr: 192.0.2.0/24

Gateway-api example

networkPolicy:
  ingressFrom:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: gateway-system
gatewayAPI:
  enabled: true
  httpRoutes:
    - parentRefs:
        - name: public
          namespace: gateway-system
      hostnames:
        - notes.example.com

External-secrets example

# SPDX-License-Identifier: Apache-2.0
auth:
  existingSecret: siyuan-auth
externalSecrets:
  enabled: true
  items:
    - fullnameOverride: siyuan-auth
      spec:
        secretStoreRef:
          name: production
          kind: ClusterSecretStore
        data:
          - secretKey: access-code
            remoteRef:
              key: applications/siyuan/access-code

Gateway API contract

Use gatewayAPI.enabled and gatewayAPI.httpRoutes[]. Set each route’s parentRefs to a shared Gateway that allows this namespace, and configure its HTTPS listener and public hostname. Routes accept labels, annotations and rules with matches, filters and optional backend references; omitted backends target this chart’s application Service. Ingress and HTTPRoute resources can coexist. Verify controller conditions and public traffic before production use. See the Gateway API documentation.