Thorn Tech Marketing Ad
Skip to main content
Version: 1.3.0

Deploy StorageLink with Helm Chart on AKS

TLDR - Quick Summary

What: Deploy StorageLink on AKS using a Helm chart with bundled or external PostgreSQL

Steps: Create AKS cluster, download the chart, run helm install

Quick start:

helm install storagelink ./storagelink \
--namespace storagelink --create-namespace \
--set license.key=trial \
--set postgresql.auth.password=$(openssl rand -hex 16) \
--set admin.username=admin \
--set admin.password='YourSecurePassword1!'

Overview​

The StorageLink Helm chart simplifies deploying StorageLink on Azure Kubernetes Service (AKS). It handles creating all Kubernetes resources (Deployments, Services, ConfigMaps, Secrets, PVCs, ServiceAccounts, NetworkPolicies, PodDisruptionBudgets) and supports two database modes:

  • Bundled PostgreSQL (default) — runs a PostgreSQL container inside the cluster using the Bitnami subchart. Good for testing and simple deployments.
  • External database — connects to a managed Azure Database for PostgreSQL Flexible Server. Recommended for production.
Container Reference

For detailed configuration options (TLS modes, client IP preservation, security context, LFS mode, image verification, and more), see the Container Deployment Reference.

Architecture​

                    ┌──────────────────────────────────────────────┐
│ Azure Kubernetes Service (AKS) │
│ Namespace: storagelink │
│ │
HTTPS (443) │ ┌────────────┐ ┌────────────────┐ │
──────────────────┼──►│ Admin UI │───►│ Backend │ │
│ │ (Nginx) │ │ (Spring Boot) │ │
│ │ │ │ Port 8080 API │ │
│ └────────────┘ └───────┬────────┘ │
│ │ │
│ ┌───────▼────────┐ │
│ │ PostgreSQL 16 │ │
│ │ (bundled or │ │
│ │ external) │ │
│ └────────────────┘ │
└──────────────────────────────────────────────┘

Components:

  • Backend — StorageLink API server (Spring Boot). Handles cloud storage operations and user management.
  • Admin UI — Web-based administration dashboard (Nginx). Exposed via a LoadBalancer on port 443.
  • PostgreSQL — Stores user accounts and configuration data.

Prerequisites​

Download the Helm chart​

Download the StorageLink Helm chart archive and extract it:

curl -LO https://help.thorntech.com/storagelink/downloads/storagelink-azure-0.3.0.tgz
tar xzf storagelink-azure-0.3.0.tgz

This creates a storagelink/ directory containing the chart (version 0.3.0, StorageLink 1.3.0), with AKS defaults already set. All helm install commands below reference this directory as ./storagelink.

Step 1: Create an AKS cluster​

# Create a resource group
az group create --name storagelink-rg --location centralus

# Create the AKS cluster
az aks create \
--resource-group storagelink-rg \
--name storagelink-aks \
--node-count 2 \
--node-vm-size Standard_DS2_v2 \
--generate-ssh-keys

# Get credentials for kubectl
az aks get-credentials --resource-group storagelink-rg --name storagelink-aks

Verify the cluster is ready:

kubectl get nodes

Step 2: Create the namespace​

kubectl create namespace storagelink
kubectl label namespace storagelink pod-security.kubernetes.io/enforce=restricted

The chart is compatible with the restricted Pod Security Standard, so enforcing it is recommended.

The images are public on Docker Hub (thorntech/storagelink-backend and thorntech/storagelink-ui), so no pull secret is needed. The tag defaults to the chart's appVersion (1.3.0). To pull from your own registry or mirror, set backend.image.repository, ui.image.repository and, if the registry needs credentials, imagePullSecrets.

Step 3: Install the Helm chart​

Option A: Bundled PostgreSQL (quickstart)​

This is the simplest option — a PostgreSQL container runs alongside the backend inside your cluster.

helm install storagelink ./storagelink \
--namespace storagelink \
--set license.key=trial \
--set postgresql.auth.password=$(openssl rand -hex 16) \
--set admin.username=admin \
--set admin.password='YourSecurePassword1!'

After a few minutes, all pods should be running:

kubectl get pods -n storagelink

Expected output:

NAME                                   READY   STATUS    RESTARTS   AGE
storagelink-backend-xxxxx 1/1 Running 0 2m
storagelink-backend-yyyyy 1/1 Running 0 2m
storagelink-postgresql-0 1/1 Running 0 2m
storagelink-ui-xxxxx 1/1 Running 0 2m
storagelink-ui-yyyyy 1/1 Running 0 2m

Option B: Azure Database for PostgreSQL (production)​

For production deployments, use a managed Azure Database for PostgreSQL Flexible Server.

Create the database​

# Create a PostgreSQL Flexible Server
az postgres flexible-server create \
--name storagelink-pg \
--resource-group storagelink-rg \
--location centralus \
--admin-user swiftgw \
--admin-password '<your-strong-password>' \
--sku-name Standard_B1ms \
--tier Burstable \
--version 16 \
--storage-size 32 \
--database-name swift_gw \
--yes

# Allow the ltree extension (required by StorageLink)
az postgres flexible-server parameter set \
--server-name storagelink-pg \
--resource-group storagelink-rg \
--name azure.extensions \
--value LTREE

# Allow Azure services (including AKS) to connect
az postgres flexible-server firewall-rule create \
--name storagelink-pg \
--resource-group storagelink-rg \
--rule-name AllowAzureServices \
--start-ip-address 0.0.0.0 \
--end-ip-address 0.0.0.0
caution

The ltree extension must be allowlisted before deploying StorageLink. Without it, the backend will fail to start because the database migration cannot create the extension.

Install with external database​

helm install storagelink ./storagelink \
--namespace storagelink \
--set license.key=trial \
--set postgresql.enabled=false \
--set externalDatabase.host=storagelink-pg.postgres.database.azure.com \
--set externalDatabase.password='<your-strong-password>' \
--set externalDatabase.jdbcParams='?sslmode=require' \
--set admin.username=admin \
--set admin.password='YourSecurePassword1!'

Step 4: Access the Admin UI​

Get the external IP of the UI service:

kubectl get svc storagelink-ui -n storagelink

It may take 1–2 minutes for Azure to assign the external IP. Once available:

NAME             TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)         AGE
storagelink-ui LoadBalancer 10.0.83.16 20.112.198.110 443:30121/TCP 2m

Open https://<EXTERNAL-IP> in your browser. You will see a certificate warning because the chart generates a self-signed TLS certificate by default — this is expected for testing.

If you provided admin.username and admin.password, log in with those credentials. Otherwise, you will be prompted to create your admin account on first access.

caution

If no admin credentials were provided and the UI is publicly accessible, the first person to reach the UI can create the admin account. See Security considerations below.

Security considerations​

Admin account​

The admin.username and admin.password values trigger a post-install Helm Job that waits for the backend to become healthy, then calls the setup API to create the admin account. If an admin account already exists (e.g., on helm upgrade), the Job detects this and exits successfully without changes.

Restrict Admin UI access​

Lock down the Admin UI to specific IP addresses using loadBalancerSourceRanges:

helm install storagelink ./storagelink \
--namespace storagelink \
--set 'ui.service.loadBalancerSourceRanges[0]=203.0.113.50/32' \
--set 'ui.service.loadBalancerSourceRanges[1]=198.51.100.0/24' \
# ... other values

Built-in hardening​

The Helm chart includes these security defaults:

  • Non-root containers — the backend runs as the image's own user (UID 1000, group 0; the chart does not pin runAsUser), the UI as UID 1000
  • Read-only root filesystem — enabled on the backend container
  • Dropped capabilities — all Linux capabilities are dropped
  • NetworkPolicies — restrict pod-to-pod and egress traffic
  • ServiceAccounts — dedicated accounts with token automount disabled
  • PodDisruptionBudgets — ensure availability during node maintenance
  • Backend not externally exposed — backend Service is ClusterIP, preventing X-Forwarded-For spoofing that would defeat login lockout and forge audit source IPs
  • Client IP preservation — UI Service uses externalTrafficPolicy: Local to preserve the client's real source IP for audit logging and login lockout
  • Zero-downtime rolling updates — UI Deployment uses maxSurge: 1, maxUnavailable: 0 to avoid dropped connections under the Local traffic policy

OpenShift​

OpenShift's restricted-v2 SecurityContextConstraint assigns user and group IDs from the namespace's range and rejects pods that set their own. Install with:

--set global.compatibility.openshift.adaptSecurityContext=force

This removes every hard-coded UID, GID and fsGroup from the chart, including the bundled PostgreSQL. Use it only on OpenShift: on EKS, AKS and GKE the IDs are required (the fsGroup makes block volumes writable).

Custom TLS certificate​

To use your own TLS certificate instead of the auto-generated self-signed one:

helm install storagelink ./storagelink \
--namespace storagelink \
--set-file ui.tls.certificate=path/to/tls.crt \
--set-file ui.tls.privateKey=path/to/tls.key \
# ... other values

Configuration reference​

Key values​

ParameterDescriptionDefault
backend.image.repositoryBackend image. Public on Docker Hub; override to use your own registry or mirrorthorntech/storagelink-backend
backend.image.tagBackend image tag. Empty uses the chart's appVersion (1.3.0); that tag must exist in the repository""
ui.image.repositoryUI image. Public on Docker Hub; override to use your own registry or mirrorthorntech/storagelink-ui
ui.image.tagUI image tag. Empty uses the chart's appVersion (1.3.0)""
imagePullSecretsPull secrets, only needed for a private registry or mirror[]
admin.usernameInitial admin username""
admin.passwordInitial admin password""
license.keyLicense for unattended installs (trial for evaluation, or your license). If empty, license the deployment from the admin UI after the first sign-in (Settings → License): start the free 30-day trial or activate a purchased license. Until then non-administrator users cannot sign in and file transfers are blocked. See Purchasing a License.""
config.cloudProviderCloud provider hint: azure, aws, or gcpazure
backend.replicaCountNumber of backend replicas2
ui.replicaCountNumber of UI replicas2
ui.tls.certificateCustom TLS certificate (PEM)"" (self-signed)
ui.tls.privateKeyCustom TLS private key (PEM)""
ui.service.loadBalancerSourceRangesRestrict Admin UI to specific IPs[]
ui.externalTlsTerminationSkip in-pod TLS when a load balancer handles TLSfalse
postgresql.enabledUse bundled PostgreSQLtrue
postgresql.auth.passwordBundled PostgreSQL password""
externalDatabase.hostExternal PostgreSQL hostname""
externalDatabase.passwordExternal PostgreSQL password""
externalDatabase.jdbcParamsAdditional JDBC params (e.g., ?sslmode=require)""
localStorage.enabledStore files on a PersistentVolume instead of cloud storage (LFS mode)false
localStorage.mountPathAbsolute path the volume is mounted at — must be outside /opt/swiftgw/mnt/data
localStorage.persistence.sizeSize of the PersistentVolumeClaim50Gi
localStorage.persistence.storageClassNameStorage class for the LFS volume. Must support ReadWriteMany — see Local File Storageazurefile
localStorage.persistence.accessModeMust be ReadWriteMany when running more than one backend replicaReadWriteMany
networkPolicy.enabledEnable NetworkPoliciestrue
podDisruptionBudget.enabledEnable PodDisruptionBudgetstrue
global.compatibility.openshift.adaptSecurityContextSet force on OpenShift — see OpenShiftunset (disabled)

Local File Storage​

Local File Storage (LFS) mode stores files on a PersistentVolume instead of cloud storage. Enable it with --set localStorage.enabled=true. The chart creates the storagelink-data PVC, mounts it at /mnt/data on every backend replica, and creates the first storage connection pointing at it on first launch.

On AKS the chart defaults the LFS volume to the azurefile storage class (Azure Files), which AKS preinstalls — no setup is needed. To use a different class, set localStorage.persistence.storageClassName; it must support ReadWriteMany.

The volume must be ReadWriteMany, because both backend replicas read and write the same files. For mount-path rules, see Local File Storage (LFS) Mode.

Upgrading​

helm upgrade storagelink ./storagelink \
--namespace storagelink \
--reuse-values
danger

If you used --set flags during install, you must pass the same values during upgrade (or use --reuse-values). Helm does not persist --set values between releases.

Upgrading from chart 0.2.0​

Chart 0.2.0 left the LFS storage class empty, so Kubernetes filled in the cluster's default class when it created the PVC. 0.3.0 defaults to azurefile, and a PVC's storage class cannot be changed after it is created. If you enabled localStorage on 0.2.0, helm upgrade fails until you do one of the following:

  • PVC Pending (the default class is ReadWriteOnce, so it usually never bound): it holds no data, so delete it before upgrading:

    kubectl delete pvc storagelink-data -n storagelink
  • PVC Bound: keep it and pin the class it already has:

    kubectl get pvc storagelink-data -n storagelink -o jsonpath='{.spec.storageClassName}'
    helm upgrade storagelink ./storagelink --namespace storagelink --reuse-values \
    --set localStorage.persistence.storageClassName=<that-class>

If localStorage was never enabled, upgrade normally.

Uninstalling​

helm uninstall storagelink --namespace storagelink

helm uninstall leaves two PersistentVolumeClaims in place, so your data is not deleted:

  • data-storagelink-postgresql-0 — the bundled PostgreSQL database, if used
  • storagelink-data — the LFS volume, if localStorage was enabled (the chart marks it helm.sh/resource-policy: keep)

A reinstall reattaches them only if it uses the same release name (storagelink) and namespace. With the bundled PostgreSQL, also pass the same postgresql.auth.password, or the backend cannot open the kept database.

To remove them once the data is no longer needed:

kubectl delete pvc data-storagelink-postgresql-0 storagelink-data -n storagelink
danger

Deleting the PersistentVolumeClaims permanently deletes the underlying volumes (Azure Managed Disks, and the LFS file share) and all data stored on them. Back up any important data first.

Troubleshooting​

Backend pod CrashLoopBackOff​

Check the backend logs:

kubectl logs -n storagelink -l app.kubernetes.io/component=backend --tail=50

Common causes:

  • ltree extension not allowlisted — See the Azure Database setup section above.
  • Database connection failed — Verify credentials and that the PostgreSQL server is running and accessible.
  • Insufficient memory — The backend requires at least 2Gi of memory.

Backend stuck on "Waiting for changelog lock"​

This happens when a previous backend instance crashed during database migrations. The Helm chart includes an init container that clears stale locks automatically on every pod start.

PVCs stuck in Pending​

kubectl get storageclass
kubectl describe pvc -n storagelink
  • Bundled PostgreSQL (data-storagelink-postgresql-0) uses the cluster's default storage class.
  • LFS volume (storagelink-data) uses azurefile. If the class is missing or misconfigured, the PVC stays Pending and so do the backend pods — see Local File Storage. If you upgraded from chart 0.2.0, see Upgrading from chart 0.2.0.

UI pod CrashLoopBackOff​

Check the UI logs:

kubectl logs -n storagelink -l app.kubernetes.io/component=ui --tail=50

Common causes:

  • TLS certificate issue — The chart auto-generates a self-signed certificate. If you provided a custom certificate, verify both ui.tls.certificate and ui.tls.privateKey are valid PEM and match each other.

External IP stuck on \<pending>​

kubectl describe svc storagelink-ui -n storagelink

Check the Events section for error messages. Common causes include insufficient permissions on the AKS managed identity or Azure subscription quota limits.

Backend pod not becoming Ready​

The backend takes 60–90 seconds to start on first launch due to database migrations. Check readiness probe status:

kubectl describe pod -n storagelink -l app.kubernetes.io/component=backend

Resource summary​

ComponentMemory (request/limit)CPU (request/limit)Storage
PostgreSQL (bundled)256Mi / 512Mi250m / 500m20Gi
Backend (x2)2Gi / 3Gi500m / 1500m—
Admin UI (x2)128Mi / 256Mi100m / 500m—
Total~4.5Gi / ~7Gi1450m / 4000m20Gi