Deploy StorageLink with Helm Chart on EKS
TLDR - Quick Summary
What: Deploy StorageLink on EKS using a Helm chart with bundled or external PostgreSQL
Steps: Create EKS cluster, download the chart, run
helm installQuick 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 Amazon Elastic Kubernetes Service (EKS). 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 Amazon RDS for PostgreSQL instance. Recommended for production.
For detailed configuration options (TLS modes, client IP preservation, security context, LFS mode, image verification, and more), see the Container Deployment Reference.
Architecture
┌──────────────────────────────────────────────┐
│ Amazon Elastic Kubernetes Service (EKS) │
│ 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
- An AWS account
- AWS CLI installed and configured
- eksctl installed
- Helm 3 installed
- kubectl installed
- Network access to Docker Hub, where the StorageLink images are published:
thorntech/storagelink-backendandthorntech/storagelink-ui
Download the Helm chart
Download the StorageLink Helm chart archive and extract it:
curl -LO https://help.thorntech.com/storagelink/downloads/storagelink-aws-0.3.0.tgz
tar xzf storagelink-aws-0.3.0.tgz
This creates a storagelink/ directory containing the chart (version 0.3.0, StorageLink 1.3.0), with EKS defaults already set. All helm install commands below reference this directory as ./storagelink.
Step 1: Create an EKS cluster
eksctl create cluster \
--name storagelink-eks \
--region us-east-1 \
--node-type t3.medium \
--nodes 2 \
--managed
This takes approximately 15 minutes. eksctl automatically creates the VPC, subnets, IAM roles, and node group.
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: Amazon RDS for PostgreSQL (production)
For production deployments, use a managed Amazon RDS for PostgreSQL instance.
Create the database
aws rds create-db-instance \
--db-instance-identifier storagelink-pg \
--db-instance-class db.t3.micro \
--engine postgres \
--engine-version 16 \
--master-username swiftgw \
--master-user-password '<your-strong-password>' \
--allocated-storage 20 \
--db-name swift_gw \
--no-multi-az \
--region us-east-1
Wait for the instance to become available:
aws rds wait db-instance-available \
--db-instance-identifier storagelink-pg \
--region us-east-1
Get the endpoint:
aws rds describe-db-instances \
--db-instance-identifier storagelink-pg \
--region us-east-1 \
--query 'DBInstances[0].Endpoint.Address' \
--output text
Configure network access
The RDS instance must be reachable from your EKS nodes. If they are in the same VPC, add the EKS node security group to the RDS security group's inbound rules on port 5432. If they are in different VPCs, either:
- Set up VPC peering between the EKS and RDS VPCs (recommended)
- Create the RDS instance in the same VPC and subnets as EKS
Consult your network team for the approach that best fits your organization's security requirements.
Install with external database
helm install storagelink ./storagelink \
--namespace storagelink \
--set license.key=trial \
--set postgresql.enabled=false \
--set externalDatabase.host=storagelink-pg.xxxxxxxxxxxx.us-east-1.rds.amazonaws.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 hostname of the UI service:
kubectl get svc storagelink-ui -n storagelink
It may take 1–2 minutes for AWS to provision the load balancer. Once available:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
storagelink-ui LoadBalancer 10.100.29.73 a71865...us-east-1.elb.amazonaws.com 443:30749/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.
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, preventingX-Forwarded-Forspoofing that would defeat login lockout and forge audit source IPs - Client IP preservation — UI Service uses
externalTrafficPolicy: Localto preserve the client's real source IP for audit logging and login lockout - Zero-downtime rolling updates — UI Deployment uses
maxSurge: 1, maxUnavailable: 0to avoid dropped connections under theLocaltraffic 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
| Parameter | Description | Default |
|---|---|---|
backend.image.repository | Backend image. Public on Docker Hub; override to use your own registry or mirror | thorntech/storagelink-backend |
backend.image.tag | Backend image tag. Empty uses the chart's appVersion (1.3.0); that tag must exist in the repository | "" |
ui.image.repository | UI image. Public on Docker Hub; override to use your own registry or mirror | thorntech/storagelink-ui |
ui.image.tag | UI image tag. Empty uses the chart's appVersion (1.3.0) | "" |
imagePullSecrets | Pull secrets, only needed for a private registry or mirror | [] |
admin.username | Initial admin username | "" |
admin.password | Initial admin password | "" |
license.key | License 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.cloudProvider | Cloud provider hint: azure, aws, or gcp | aws |
backend.replicaCount | Number of backend replicas | 2 |
ui.replicaCount | Number of UI replicas | 2 |
ui.tls.certificate | Custom TLS certificate (PEM) | "" (self-signed) |
ui.tls.privateKey | Custom TLS private key (PEM) | "" |
ui.service.loadBalancerSourceRanges | Restrict Admin UI to specific IPs | [] |
ui.externalTlsTermination | Skip in-pod TLS when a load balancer handles TLS | false |
postgresql.enabled | Use bundled PostgreSQL | true |
postgresql.auth.password | Bundled PostgreSQL password | "" |
externalDatabase.host | External PostgreSQL hostname | "" |
externalDatabase.password | External PostgreSQL password | "" |
externalDatabase.jdbcParams | Additional JDBC params (e.g., ?sslmode=require) | "" |
localStorage.enabled | Store files on a PersistentVolume instead of cloud storage (LFS mode) | false |
localStorage.mountPath | Absolute path the volume is mounted at — must be outside /opt/swiftgw | /mnt/data |
localStorage.persistence.size | Size of the PersistentVolumeClaim | 50Gi |
localStorage.persistence.storageClassName | Storage class for the LFS volume. Must support ReadWriteMany — see Local File Storage | efs-sc |
localStorage.persistence.accessMode | Must be ReadWriteMany when running more than one backend replica | ReadWriteMany |
networkPolicy.enabled | Enable NetworkPolicies | true |
podDisruptionBudget.enabled | Enable PodDisruptionBudgets | true |
global.compatibility.openshift.adaptSecurityContext | Set force on OpenShift — see OpenShift | unset (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 EKS the chart defaults the LFS volume to the efs-sc storage class (Amazon EFS). EKS does not create it for you:
- Create an EFS file system in your cluster's VPC, with a mount target in each subnet your nodes use and a security group that allows NFS (TCP 2049) from the nodes.
- Install the Amazon EFS CSI driver (EKS add-on
aws-efs-csi-driver). - Create the
efs-scstorage class. It must setprovisioningMode— a class with onlyfileSystemIdleaves the PVCPendingwithMissing provisioningMode parameter:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: efs-sc
provisioner: efs.csi.aws.com
parameters:
provisioningMode: efs-ap # one EFS access point per PVC
fileSystemId: fs-0123456789abcdef0
directoryPerms: "700" # the access point's POSIX user owns the directory
To use a different class, set localStorage.persistence.storageClassName.
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
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 efs-sc, 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 isReadWriteOnce, so it usually never bound): it holds no data, so delete it before upgrading:kubectl delete pvc storagelink-data -n storagelinkPVC
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 usedstoragelink-data— the LFS volume, iflocalStoragewas enabled (the chart marks ithelm.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
Deleting the PersistentVolumeClaims permanently deletes the underlying volumes (EBS volumes, 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:
- Database connection failed — Verify credentials and that the RDS instance is running and accessible from the EKS nodes. Check security group rules.
- 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) usesefs-sc. If the class is missing or misconfigured, the PVC staysPendingand 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.certificateandui.tls.privateKeyare 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. On EKS, the LoadBalancer creates a Classic Load Balancer (ELB) by default. Ensure your EKS node security group allows inbound traffic on the NodePort range.
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
| Component | Memory (request/limit) | CPU (request/limit) | Storage |
|---|---|---|---|
| PostgreSQL (bundled) | 256Mi / 512Mi | 250m / 500m | 20Gi |
| Backend (x2) | 2Gi / 3Gi | 500m / 1500m | — |
| Admin UI (x2) | 128Mi / 256Mi | 100m / 500m | — |
| Total | ~4.5Gi / ~7Gi | 1450m / 4000m | 20Gi |
