Thorn Tech Marketing Ad
Skip to main content
Version: 1.2.1

Deploy StorageLink with Helm Chart on GKE

TLDR - Quick Summary

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

Steps: Create GKE cluster, configure image access, run helm install

Quick start:

helm install storagelink ./storagelink \
--namespace storagelink --create-namespace \
--set backend.image.repository=<backend-image> \
--set backend.image.tag=<tag> \
--set ui.image.repository=<ui-image> \
--set ui.image.tag=<tag> \
--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 Google Kubernetes Engine (GKE). 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 Cloud SQL for PostgreSQL instance. 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

                    ┌──────────────────────────────────────────────┐
│ Google Kubernetes Engine (GKE) │
│ 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

  • A Google Cloud project with billing enabled
  • gcloud CLI installed and configured
  • Helm 3 installed
  • kubectl installed
  • Access to StorageLink container images (provided per your engagement)

Download the Helm chart

Download the StorageLink Helm chart archive and extract it:

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

This creates a storagelink/ directory containing the chart. All helm install commands below reference this directory as ./storagelink.

Step 1: Create a GKE cluster

gcloud container clusters create storagelink-gke \
--zone us-central1-a \
--num-nodes 2 \
--machine-type e2-standard-2 \
--enable-network-policy

Get credentials for kubectl:

gcloud container clusters get-credentials storagelink-gke --zone us-central1-a

Verify the cluster is ready:

kubectl get nodes

Step 2: Set up image access

StorageLink container images are delivered per your engagement. Create a pull secret so Kubernetes can download them:

kubectl create namespace storagelink

kubectl create secret docker-registry storagelink-pull-secret \
--namespace storagelink \
--docker-server=<registry-url> \
--docker-username=<username> \
--docker-password=<password>

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 backend.image.repository=<backend-image> \
--set backend.image.tag=<tag> \
--set ui.image.repository=<ui-image> \
--set ui.image.tag=<tag> \
--set license.key=trial \
--set 'imagePullSecrets[0].name=storagelink-pull-secret' \
--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-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: Cloud SQL for PostgreSQL (production)

For production deployments, use a managed Cloud SQL for PostgreSQL instance.

Create the database

# Create a Cloud SQL instance
gcloud sql instances create storagelink-pg \
--database-version=POSTGRES_16 \
--tier=db-f1-micro \
--edition=enterprise \
--region=us-central1 \
--root-password='<your-strong-password>' \
--authorized-networks=0.0.0.0/0 \
--storage-size=10GB

# Create the database
gcloud sql databases create swift_gw --instance=storagelink-pg

Get the instance IP:

gcloud sql instances describe storagelink-pg \
--format="value(ipAddresses[0].ipAddress)"
tip

For production, use a private IP or the Cloud SQL Auth Proxy instead of --authorized-networks=0.0.0.0/0.

Install with external database

helm install storagelink ./storagelink \
--namespace storagelink \
--set backend.image.repository=<backend-image> \
--set backend.image.tag=<tag> \
--set ui.image.repository=<ui-image> \
--set ui.image.tag=<tag> \
--set license.key=trial \
--set 'imagePullSecrets[0].name=storagelink-pull-secret' \
--set postgresql.enabled=false \
--set externalDatabase.host=<cloud-sql-ip> \
--set externalDatabase.username=postgres \
--set externalDatabase.password='<your-strong-password>' \
--set externalDatabase.jdbcParams='?sslmode=require' \
--set admin.username=admin \
--set admin.password='YourSecurePassword1!'
note

Cloud SQL uses the postgres user by default, unlike the bundled PostgreSQL which uses swiftgw.

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 GCP to assign the external IP. Once available:

NAME             TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)         AGE
storagelink-ui LoadBalancer 34.118.236.101 34.172.240.196 443:30971/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 — backend runs as UID 100, UI runs 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

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 container image""
backend.image.tagBackend image tag""
ui.image.repositoryUI container image""
ui.image.tagUI image tag""
admin.usernameInitial admin username""
admin.passwordInitial admin password""
license.keyStorageLink license key (use trial for evaluation)""
config.cloudProviderCloud provider hint: azure, aws, or gcpgcp
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)""
networkPolicy.enabledEnable NetworkPoliciestrue
podDisruptionBudget.enabledEnable PodDisruptionBudgetstrue

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.

Uninstalling

helm uninstall storagelink --namespace storagelink

# PVCs are not deleted automatically — remove if no longer needed:
kubectl delete pvc --all -n storagelink
danger

Deleting the PersistentVolumeClaims will permanently delete the underlying Persistent Disks and all data stored on them. Back up any important data before uninstalling.

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 Cloud SQL instance is running and accessible. Check authorized networks.
  • 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

GKE uses the standard-rwo storage class by default:

kubectl get storageclass
kubectl describe pvc -n storagelink

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 project quota or the GKE cluster lacking the necessary permissions to create external load balancers.

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