↓ Skip to main content

Deploying Syncthing on Kubernetes with K3s and Helm

·687 words·4 mins
Syncthing logo

Overview
#

This document outlines the deployment of Syncthing on a K3s cluster using the k8s-home-lab-repo Helm chart. It specifically details how to configure hostPath storage for existing local data, manage container permissions to prevent access errors, and avoid common Helm merge conflicts during deployment.

Prerequisites
#

  • A functional K3s cluster.
  • Helm installed and configured to communicate with the cluster.
  • An existing directory on the host machine to be synchronized (e.g., /var/home/joeri). You only need to specify the parent directory. Syncthing will let you chose which subdirectories you want to keep in sync across different hosts.

1. Add the Helm Repository
#

Add the k8s-home-lab repository to your Helm configuration and update the local cache:

helm repo add k8s-home-lab [https://k8s-home-lab.github.io/helm-charts/](https://k8s-home-lab.github.io/helm-charts/)
helm repo update

2. Configuration Requirements and Error Handling
#

When overriding the default chart values, specific configurations are required to ensure the Syncthing container can successfully start and access the underlying storage.

Avoiding Port Conflicts (Helm Merge Behavior)
#

Syncthing requires ports 22000 (TCP/UDP) and 21027 (UDP). The base Helm chart natively defines these ports. Attempting to explicitly redefine them in a custom values.yaml under the service block will result in a server-side apply error during deployment:

duplicate entries for key [containerPort=22000,protocol="TCP"]

Resolution: Do not declare the sync or discovery ports in your custom values.yaml. Only declare the UI port (8384). Helm will merge your custom UI port definition with the chart’s default sync ports natively.

Storage Permissions and Contexts
#

If the container lacks the correct permissions to write to the underlying Kubernetes volume, it will fail to initialize its database and enter a CrashLoopBackOff state, producing the following error:

ERR Failed to ensure directory exists (error="mkdir /var/syncthing/config: permission denied" log.pkg=main)

This occurs because the volumes provisioned by K3s (local-path) are owned by root, while the Syncthing container image enforces execution as a non-root user (UID 1000).

Resolution:

  1. Apply a podSecurityContext with fsGroup: 1000 and fsGroupChangePolicy: "OnRootMismatch". This instructs Kubernetes to modify the ownership of the attached volumes.
  2. Define PUID and PGID environment variables as 1000 to satisfy the requirements of the container’s underlying initialization scripts.

Explicit Volume Mount Paths
#

The k8s-home-lab chart utilizes the bjw-s common library. By default, this library mounts persistent volumes to a path matching the volume’s declarative name (e.g., a volume named config mounts to /config). However, the official Syncthing Docker image hardcodes its working directory to /var/syncthing.

If this default behavior is not overridden, Syncthing attempts to write to the container’s root filesystem (/var/syncthing/config) rather than the attached persistent volume, resulting in a permission denied error.

Resolution: Explicitly declare mountPath: /var/syncthing within the persistence.config block.


3. Final values.yaml Configuration
#

Save the following configuration as values.yaml. Modify the host string and the hostPath to match your environment.

# 1. Pod-level security context (handles volume permissions)
podSecurityContext:
  runAsUser: 1000
  runAsGroup: 1000
  fsGroup: 1000
  fsGroupChangePolicy: "OnRootMismatch"

# 2. Container-level security context (forces container process)
securityContext:
  readOnlyRootFilesystem: false
  runAsNonRoot: true
  runAsUser: 1000
  runAsGroup: 1000

env:
  # The UI and Log timezone. File syncing relies on UTC internally.
  TZ: "Indian/Mauritius"
  # Ensures compatibility with image initialization scripts
  PUID: "1000"
  PGID: "1000"

ingress:
  main:
    enabled: true
    ingressClassName: traefik
    hosts:
      - host: syncthing.yourdomain.com # Update to your target FQDN
        paths:
          - path: /
            pathType: Prefix

persistence:
  # Standard volume for Syncthing's internal config/database
  config:
    enabled: true
    storageClass: "local-path"
    accessMode: ReadWriteOnce
    size: 2Gi
    # Overwrite the default common-library mount path
    mountPath: /var/syncthing
    
  # Direct hostPath mapping for data synchronization
  existing-data:
    enabled: true
    type: hostPath
    hostPath: /var/home/joeri # The parent directory on the K3s host of any content Syncthing will have access to via the mountPath below.
    mountPath: /data

service:
  main:
    ports:
      http:
        port: 8384

4. Deployment
#

Create a dedicated namespace and deploy the chart using the custom configuration file:

kubectl create namespace syncthing

helm install syncthing k8s-home-lab/syncthing \
  --namespace syncthing \
  -f values.yaml

To verify the deployment status:

kubectl get pods -n syncthing

Once the pod transitions to a Running state, the Syncthing Web UI will be accessible via the defined Ingress host. The host directory is mounted at /data within the container and is ready to be configured for synchronization in the UI.