Deploy StorageLink on AKS with Kubernetes Manifests
Purpose: Deploy StorageLink on Azure Kubernetes Service (AKS) with Kustomize-managed manifests and an external Azure Database for PostgreSQL Flexible Server
Requirements: Azure CLI, kubectl and the StorageLink container images (provided per your engagement)
Quick Steps:
- Create the AKS cluster and the Flexible Server, with the
ltreeextension allowlisted - Create the
storagelinknamespace and thestoragelink-pull-secretpull secret - Fill in
secrets.yaml, the server hostname inconfigmap.yamland the image references - Apply the manifests (below) and open
https://<EXTERNAL-IP>of theuiservice
Quick Start:
kubectl apply -f k8s/secrets.yaml
kubectl apply -k k8s/
License: Set LICENSE in secrets.yaml to trial for evaluation or to your license key. If unset, start the 30-day trial or activate a license in the admin UI (Settings → License).
Important: Set loadBalancerSourceRanges in ui.yaml before deploying — the first visitor to the admin UI can create the admin account
Product: StorageLink by Thorn Technologies — cloud storage gateway for secure file sharing
Overview
This guide walks through deploying StorageLink on Azure Kubernetes Service (AKS) using plain Kubernetes manifests, managed by Kustomize. This is an alternative to the Helm chart deployment — use this approach if your organization prefers raw manifests or does not use Helm.
This deployment uses an external Azure Database for PostgreSQL Flexible Server (no in-cluster database). The manifests include security hardening out of the box: NetworkPolicies, ServiceAccounts with token automount disabled, non-root containers, and IP-restricted load balancers.
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 8443 │ │ Port 8080 API │ │
│ └────────────┘ └───────┬────────┘ │
│ │ │
└─────────────────────────────┼────────────────┘
│
┌───────▼──────────┐
│ Azure Database │
│ for PostgreSQL │
│ Flexible Server │
└──────────────────┘
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 — Azure-managed database storing user accounts and configuration data.
Prerequisites
- An Azure subscription
- Azure CLI installed and logged in
- kubectl installed (includes Kustomize since v1.14+)
- Network access to Docker Hub, where the StorageLink images are published:
thorntech/storagelink-backendandthorntech/storagelink-ui
Deployment files
The deployment consists of Kubernetes manifest files managed by Kustomize:
| File | Description |
|---|---|
kustomization.yaml | Kustomize orchestration — sets namespace, labels, and resource order |
namespace.yaml | Creates the storagelink namespace |
serviceaccount.yaml | Dedicated ServiceAccounts with token automount disabled |
secrets.yaml | Template for database credentials, OAuth secrets, JWT key, license, and TLS certificate |
configmap.yaml | Non-sensitive configuration for the backend and UI |
networkpolicy.yaml | NetworkPolicies restricting pod-to-pod and egress traffic |
backend.yaml | Backend Deployment and ClusterIP Service |
ui.yaml | Admin UI Deployment and LoadBalancer Service (with IP restrictions) |
components/lfs/ | Optional Kustomize component for Local File Storage mode — adds a ReadWriteMany PVC (azurefile) mounted at /mnt/data. Disabled by default. |
components/openshift/ | Optional Kustomize component for OpenShift — removes the hard-coded user and group IDs. Disabled by default. |
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 Azure PostgreSQL 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
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.
Step 3: Image access
The images are public on Docker Hub, and the manifests already reference them (thorntech/storagelink-backend:1.3.0 and thorntech/storagelink-ui:1.3.0), so no pull secret is needed. To pull from your own registry or mirror instead, see 4c.
Step 4: Configure the manifests
4a. Update secrets
The secrets.yaml file is a template with REPLACE_ME placeholders. Fill in the values and apply it separately (it is excluded from Kustomize to prevent accidental commits of real credentials).
Generate secure values:
# Database password
openssl rand -base64 32
# OAuth client ID and secret (must match between backend and UI secrets)
openssl rand -hex 16
# JWT secret
openssl rand -base64 32
Generate a self-signed TLS certificate for testing:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes \
-subj "/CN=storagelink/O=YourOrg"
Update secrets.yaml with your values:
# storagelink-db-secret
stringData:
POSTGRES_USER: swiftgw
POSTGRES_PASSWORD: <generated-password> # Must match Azure PG password
POSTGRES_DB: swift_gw
# storagelink-backend-secret
stringData:
SPRING_DATASOURCE_USERNAME: swiftgw
SPRING_DATASOURCE_PASSWORD: <generated-password> # Must match above
SECURITY_CLIENT_ID: <generated-client-id> # Must match UI secret
SECURITY_CLIENT_SECRET: <generated-client-secret> # Must match UI secret
SECURITY_JWT_SECRET: <generated-jwt-secret>
LICENSE: <your-license-key> # "trial" for evaluation, or your license key. If unset, license the deployment from the admin UI (Settings → License)
# storagelink-ui-secret
stringData:
SECURITY_CLIENT_ID: <generated-client-id> # Must match backend
SECURITY_CLIENT_SECRET: <generated-client-secret> # Must match backend
WEBSITE_BUNDLE_CRT: |
-----BEGIN CERTIFICATE-----
<paste cert.pem contents>
-----END CERTIFICATE-----
WEBSITE_KEY: |
-----BEGIN PRIVATE KEY-----
<paste key.pem contents>
-----END PRIVATE KEY-----
For production, use Azure Key Vault with the Secrets Store CSI Driver to manage secrets outside of Kubernetes manifests.
4b. Update ConfigMap
Edit configmap.yaml and replace the PostgreSQL hostname placeholder:
# storagelink-backend-config
data:
DB_HOST: storagelink-pg.postgres.database.azure.com
SPRING_DATASOURCE_URL: jdbc:postgresql://storagelink-pg.postgres.database.azure.com:5432/swift_gw?sslmode=require
4c. Container images (optional)
backend.yaml and ui.yaml pull StorageLink 1.3.0 from Docker Hub. Change the image: lines only to use a different version or your own registry or mirror. If that registry needs credentials, create a pull secret and add it to both Deployments under spec.template.spec:
imagePullSecrets:
- name: storagelink-pull-secret
4d. Configure Admin UI access restrictions
The Admin UI should be restricted to authorized IP addresses. Edit ui.yaml and configure the Service with loadBalancerSourceRanges:
spec:
type: LoadBalancer
loadBalancerSourceRanges:
- "203.0.113.50/32" # Replace with your admin's IP
- "198.51.100.0/24" # Replace with your office network (optional)
The first person to reach the Admin UI can create the admin account. Always restrict access via loadBalancerSourceRanges before deploying.
Step 5: Deploy
Deploy the manifests first. This also creates the storagelink namespace:
kubectl apply -k k8s/
Then apply the secrets (they are excluded from Kustomize so real credentials are never committed):
kubectl apply -f k8s/secrets.yaml
Until the secrets exist, the pods wait in CreateContainerConfigError; they start on their own once the secrets are applied.
Monitor the deployment:
kubectl get pods -n storagelink -w
A healthy deployment looks like:
NAME READY STATUS RESTARTS AGE
backend-xxxxx 1/1 Running 0 2m
backend-yyyyy 1/1 Running 0 2m
ui-xxxxx 1/1 Running 0 2m
ui-yyyyy 1/1 Running 0 2m
The backend may take 60–90 seconds to become ready due to database migrations on first launch.
Step 6: Access the Admin UI
Get the external IP of the UI service:
kubectl get svc 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
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 deployment uses a self-signed TLS certificate — this is expected for testing.
You will be prompted to create your admin account on first access via the setup wizard.
Optional components
Enable a component by uncommenting it under components: in k8s/kustomization.yaml, then re-run kubectl apply -k k8s/.
Local File Storage (optional)
components/lfs stores files on a PersistentVolume instead of cloud storage. It adds the storagelink-data-pvc PVC, mounts it at /mnt/data on every backend replica, and creates the first storage connection pointing at it on first launch. The volume must be ReadWriteMany, because both backend replicas read and write the same files.
The component's PVC uses the azurefile storage class (Azure Files), which AKS preinstalls — no setup is needed.
kubectl delete -k k8s/ deletes the LFS PVC, and with the default Delete reclaim policy the files go with it. For production data, use a storage class with reclaimPolicy: Retain.
OpenShift (optional)
OpenShift's restricted-v2 SecurityContextConstraint assigns user and group IDs from the namespace's range and rejects pods that set their own. components/openshift removes the hard-coded IDs so the platform assigns them. Use it only on OpenShift: on AKS the IDs are required (the fsGroup makes block volumes writable).
Initial admin account
On first access, StorageLink displays a setup wizard that lets you create the initial admin account. To prevent unauthorized access to the wizard, restrict the Admin UI to trusted IP addresses using loadBalancerSourceRanges on the UI Service.
Security features
The manifests include these security defaults:
- Non-root containers — the backend runs as the image's own user (UID 1000, group 0; the manifests do not pin
runAsUser), the UI as UID 1000 - Read-only root filesystem — enabled on the backend container
- Dropped capabilities — all Linux capabilities are dropped
- No privilege escalation —
allowPrivilegeEscalation: falseon all containers - NetworkPolicies — restrict pod-to-pod and egress traffic (DNS, PostgreSQL, HTTPS for cloud storage)
- ServiceAccounts — dedicated accounts with
automountServiceAccountToken: false - Pod anti-affinity — backend and UI replicas prefer spreading across nodes
- IP-restricted Admin UI —
loadBalancerSourceRangeson the LoadBalancer service - 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
Custom TLS certificate
Replace the self-signed certificate by updating WEBSITE_BUNDLE_CRT and WEBSITE_KEY in storagelink-ui-secret with your CA-signed certificate and key.
For production, consider using an Ingress controller with cert-manager for automated certificate management.
Configuration reference
Backend ConfigMap (storagelink-backend-config)
| Variable | Description | Example |
|---|---|---|
SPRING_PROFILES_ACTIVE | Spring profile | container,json-logs |
LOGGING_LEVEL_ROOT | Log level | INFO |
DB_HOST | PostgreSQL FQDN | storagelink-pg.postgres.database.azure.com |
SPRING_DATASOURCE_URL | Full JDBC URL | jdbc:postgresql://host:5432/swift_gw?sslmode=require |
FEATURES_INSTANCE_CLOUD_PROVIDER | Cloud provider hint | azure |
UI ConfigMap (storagelink-ui-config)
| Variable | Description | Example |
|---|---|---|
BACKEND_URL | Backend API endpoint (must include trailing slash) | http://backend:8080/ |
CLOUD_PROVIDER | Cloud provider hint | azure |
Resource summary
| Component | Memory (request/limit) | CPU (request/limit) | Storage |
|---|---|---|---|
| Backend (x2) | 2Gi / 3Gi | 500m / 1500m | — |
| Admin UI (x2) | 128Mi / 256Mi | 100m / 500m | — |
| Total | ~4.3Gi / ~6.5Gi | 1200m / 3500m | — |
The Azure Database for PostgreSQL Flexible Server resources are managed separately by Azure.
Troubleshooting
Backend pod CrashLoopBackOff
Check the backend logs:
kubectl logs -n storagelink -l app=backend --tail=50
Common causes:
ltreeextension not allowlisted — See the Azure Database setup in Step 2.- Database connection failed — Verify credentials match between
storagelink-db-secret,storagelink-backend-secret, and the Azure PostgreSQL server. Check firewall 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 init container clears stale locks automatically on every pod start. If the issue persists:
# Scale backend to 0
kubectl scale deployment/backend -n storagelink --replicas=0
# Clear the lock manually
kubectl run -n storagelink pg-fix --rm -it --restart=Never \
--image=postgres:16-alpine --env="PGPASSWORD=<password>" \
-- psql -h <pg-server>.postgres.database.azure.com -U swiftgw -d swift_gw \
-c "UPDATE databasechangeloglock SET locked=false, lockgranted=null, lockedby=null WHERE id=1;"
# Scale backend back up
kubectl scale deployment/backend -n storagelink --replicas=1
UI pod CrashLoopBackOff
Check the UI logs:
kubectl logs -n storagelink -l app=ui --tail=50
Common causes:
- Invalid TLS certificate — Verify both
WEBSITE_BUNDLE_CRTandWEBSITE_KEYare valid PEM and match each other. Regenerate both together if needed. - Certificate/key mismatch — If you see
SSL_CTX_use_PrivateKey ... key values mismatch, the private key does not correspond to the certificate.
ImagePullBackOff
kubectl describe pod -n storagelink -l app=backend
Check the Events section. With the default Docker Hub images, make sure the nodes can reach registry-1.docker.io (NAT or egress rules) and that the tag exists. Anonymous Docker Hub pulls are rate-limited; if you hit the limit, use a registry mirror. If you changed the images to a private registry, check that the pull secret exists in the storagelink namespace and is listed under imagePullSecrets.
External IP stuck on \<pending>
kubectl describe svc 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.
Uninstalling
# Remove secrets
kubectl delete -f k8s/secrets.yaml
# Remove all other resources
kubectl delete -k k8s/
If components/lfs is enabled, this also deletes the LFS PVC and, with the default Delete reclaim policy, the files on it. Back them up first.
To also remove the Azure PostgreSQL server:
az postgres flexible-server delete --name storagelink-pg --resource-group storagelink-rg --yes
Manifest files
Create a k8s/ directory and add the following files. The step-by-step instructions above explain which values to customize.
k8s/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: storagelink
labels:
- pairs:
app.kubernetes.io/name: storagelink
app.kubernetes.io/version: "1.3.0"
resources:
- namespace.yaml
- serviceaccount.yaml
# secrets.yaml is a template — apply real secrets separately via kubectl
- configmap.yaml
- networkpolicy.yaml
- backend.yaml
- ui.yaml
# Local File Storage (LFS) mode is off by default — the backend is stateless and
# stores files in cloud storage. To store files on a PersistentVolume instead,
# uncomment the component below. It adds a ReadWriteMany PVC, mounts it at
# /mnt/data, and pre-configures the first storage connection to use it.
#
# The base prefix MUST be an absolute path outside /opt/swiftgw. A relative path
# is accepted but does not work: the backend cannot create its directory under
# the read-only root filesystem (see components/lfs/kustomization.yaml).
#
# On OpenShift, also add components/openshift. It removes every hard-coded
# UID, GID and fsGroup so the restricted-v2 SCC can assign them. Do not use it
# on EKS/AKS/GKE: RWO block volumes (Azure Disk, EBS, PD) need fsGroup: 0.
#
# components:
# - components/lfs
# - components/openshift
k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: storagelink
labels:
pod-security.kubernetes.io/enforce: restricted
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: storagelink-quota
spec:
hard:
requests.cpu: "4"
requests.memory: "8Gi"
limits.cpu: "8"
limits.memory: "16Gi"
pods: "20"
---
apiVersion: v1
kind: LimitRange
metadata:
name: storagelink-limits
spec:
limits:
- default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
type: Container
k8s/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: storagelink-backend
automountServiceAccountToken: false
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: storagelink-ui
automountServiceAccountToken: false
k8s/secrets.yaml
# DO NOT commit this file with real values — it is in .gitignore.
# This is a template. Fill in values and apply with: kubectl apply -f secrets.yaml
#
# Generate secure values:
# Password: openssl rand -base64 32
# Client ID: openssl rand -hex 16
# Client Secret: openssl rand -hex 16
# JWT Secret: openssl rand -base64 32
#
# For production, use Azure Key Vault with the Secrets Store CSI Driver:
# https://learn.microsoft.com/en-us/azure/aks/csi-secrets-store-driver
apiVersion: v1
kind: Secret
metadata:
name: storagelink-db-secret
namespace: storagelink
type: Opaque
stringData:
POSTGRES_USER: "REPLACE_ME"
POSTGRES_PASSWORD: "REPLACE_ME" # openssl rand -base64 32
POSTGRES_DB: "REPLACE_ME"
---
apiVersion: v1
kind: Secret
metadata:
name: storagelink-backend-secret
namespace: storagelink
type: Opaque
stringData:
SPRING_DATASOURCE_USERNAME: "REPLACE_ME" # must match POSTGRES_USER
SPRING_DATASOURCE_PASSWORD: "REPLACE_ME" # must match POSTGRES_PASSWORD
SECURITY_CLIENT_ID: "REPLACE_ME" # openssl rand -hex 16 — must match UI secret
SECURITY_CLIENT_SECRET: "REPLACE_ME" # openssl rand -hex 16 — must match UI secret
SECURITY_JWT_SECRET: "REPLACE_ME" # openssl rand -base64 32
LICENSE: "REPLACE_ME" # "trial" for evaluation, or your license key. If unset, license the deployment from the admin UI (Settings → License)
---
apiVersion: v1
kind: Secret
metadata:
name: storagelink-ui-secret
namespace: storagelink
type: Opaque
stringData:
SECURITY_CLIENT_ID: "REPLACE_ME" # must match backend secret
SECURITY_CLIENT_SECRET: "REPLACE_ME" # must match backend secret
WEBSITE_BUNDLE_CRT: "REPLACE_ME" # PEM-encoded certificate
WEBSITE_KEY: "REPLACE_ME" # PEM-encoded private key
k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: storagelink-backend-config
data:
SPRING_PROFILES_ACTIVE: container,json-logs
LOGGING_LEVEL_ROOT: INFO
DB_HOST: REPLACE_WITH_PG_FQDN
SPRING_DATASOURCE_URL: jdbc:postgresql://REPLACE_WITH_PG_FQDN:5432/swift_gw?sslmode=require
SERVER_PORT: "8080"
FEATURES_INSTANCE_CLOUD_PROVIDER: azure
FEATURES_SYSTEMD_NOTIFY: "false"
FEATURES_SFTP_SUBSYSTEM_AUTO_START: "false"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: storagelink-ui-config
data:
BACKEND_URL: "http://backend:8080/"
CLOUD_PROVIDER: azure
k8s/networkpolicy.yaml
# NetworkPolicies restrict pod-to-pod and egress traffic.
# Without these, any pod in the cluster can reach any other pod.
# Backend: only accept traffic from UI pods on port 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-netpol
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: ui
ports:
- protocol: TCP
port: 8080
egress:
# Allow DNS resolution
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow connection to Azure PostgreSQL (port 5432)
- to: []
ports:
- protocol: TCP
port: 5432
# Allow HTTPS egress for cloud storage connections (Azure Blob, AWS S3, GCS, etc.)
- to: []
ports:
- protocol: TCP
port: 443
---
# UI: accept traffic from the LoadBalancer on port 443, talk to backend on 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ui-netpol
spec:
podSelector:
matchLabels:
app: ui
policyTypes:
- Ingress
- Egress
ingress:
- ports:
- protocol: TCP
port: 8443
egress:
# Allow DNS resolution
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow connection to backend
- to:
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 8080
k8s/backend.yaml
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
type: ClusterIP
selector:
app: backend
ports:
- name: http
port: 8080
targetPort: 8080
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: backend-pdb
spec:
maxUnavailable: 1
selector:
matchLabels:
app: backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
spec:
replicas: 2
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
serviceAccountName: storagelink-backend
securityContext:
runAsNonRoot: true
# GID 0 matches the group-0 setgid ownership of /opt/swiftgw in the image.
# fsGroup also fixes up volume ownership, without which a PVC that arrives
# root:root 0755 (RWO block volumes: Azure Disk, EBS, PD) is unwritable by
# the backend. Required on EKS/AKS/GKE; components/openshift removes it.
fsGroup: 0
# Only chown the volume when its root ownership is wrong, instead of
# recursively on every pod start (slow on a large LFS volume).
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: backend
topologyKey: kubernetes.io/hostname
initContainers:
- name: wait-for-db
image: postgres:16-alpine
securityContext:
runAsUser: 70 # postgres user in alpine image
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: storagelink-backend-config
key: DB_HOST
envFrom:
- secretRef:
name: storagelink-db-secret
command:
- sh
- -c
- |
until pg_isready -h "$DB_HOST" -p 5432 -U "$POSTGRES_USER"; do
echo "Waiting for PostgreSQL..."
sleep 2
done
echo "PostgreSQL is ready"
echo "Clearing any stale Liquibase lock..."
PGPASSWORD="$POSTGRES_PASSWORD" psql -h "$DB_HOST" -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
-c "UPDATE databasechangeloglock SET locked=false, lockgranted=null, lockedby=null WHERE id=1;" 2>/dev/null || true
containers:
- name: backend
image: thorntech/storagelink-backend:1.3.0
imagePullPolicy: Always
securityContext:
# Deliberately no runAsUser: the image owns the UID (USER 1000:0).
# SWIFT-1450 removed the swiftgw user so the image runs under an
# arbitrary UID; do not re-pin it here. Only safe because the
# Dockerfiles use a numeric USER; a named user fails runAsNonRoot
# admission with "cannot verify user is non-root". SCCs have no
# runAsGroup strategy; components/openshift removes it with the other
# IDs so the platform assigns all of them.
runAsGroup: 0
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
ports:
- containerPort: 8080
envFrom:
- secretRef:
name: storagelink-backend-secret
- configMapRef:
name: storagelink-backend-config
resources:
requests:
memory: "2Gi"
cpu: "500m"
limits:
memory: "3Gi"
cpu: "1500m"
volumeMounts:
- name: tmp
mountPath: /tmp
startupProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
failureThreshold: 30
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 180
periodSeconds: 20
volumes:
- name: tmp
emptyDir: {}
k8s/ui.yaml
apiVersion: v1
kind: Service
metadata:
name: ui
spec:
type: LoadBalancer
# Local preserves client IP; Cluster allows cross-node balancing but SNATs the source.
externalTrafficPolicy: Local
loadBalancerSourceRanges:
- "REPLACE_WITH_YOUR_CIDR" # e.g. "203.0.113.0/24" — restrict to your network
selector:
app: ui
ports:
- name: https
port: 443
targetPort: 8443
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: ui-pdb
spec:
maxUnavailable: 1
selector:
matchLabels:
app: ui
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ui
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: ui
template:
metadata:
labels:
app: ui
spec:
serviceAccountName: storagelink-ui
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: ui
topologyKey: kubernetes.io/hostname
containers:
- name: ui
image: thorntech/storagelink-ui:1.3.0
imagePullPolicy: Always
securityContext:
runAsUser: 1000 # image default user that owns writable directories
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false # UI entrypoint writes to /etc/nginx/templates, /etc/nginx/conf.d, /var/run/swiftgw-webconfig
capabilities:
drop: ["ALL"]
ports:
- containerPort: 8443
envFrom:
- secretRef:
name: storagelink-ui-secret
- configMapRef:
name: storagelink-ui-config
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
volumeMounts:
- name: tmp
mountPath: /tmp
readinessProbe:
httpGet:
path: /
port: 8443
scheme: HTTPS
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 8443
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 20
volumes:
- name: tmp
emptyDir: {}
k8s/components/lfs/kustomization.yaml
Only needed for Local File Storage.
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
# Local File Storage (LFS) mode — stores files on a PersistentVolume instead of
# cloud storage. Opt in by adding this to the top-level kustomization.yaml:
#
# components:
# - components/lfs
#
# The mount path MUST be an absolute path outside the application home
# (/opt/swiftgw). A relative path is accepted, but the backend cannot create
# its directory under the read-only root filesystem: it logs the error and
# first launch creates a connection to a directory that does not exist. See
# the Container Reference KB article.
#
# WARNING: `kubectl delete -k` deletes the PVC (directly, and again with the
# namespace), and the cloud default reclaim policy is Delete, so the files go
# with it. Unlike Helm there is no keep annotation. For production data use a
# StorageClass with reclaimPolicy: Retain.
resources:
- pvc.yaml
patches:
# Pre-configure the first storage connection as LFS.
- target:
kind: ConfigMap
name: storagelink-backend-config
patch: |-
apiVersion: v1
kind: ConfigMap
metadata:
name: storagelink-backend-config
data:
FEATURES_FIRST_CONNECTION_CLOUD_PROVIDER: "lfs"
FEATURES_FIRST_CONNECTION_BASE_PREFIX: "/mnt/data"
# Mount the PVC on the backend pods.
- target:
kind: Deployment
name: backend
patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
spec:
template:
spec:
containers:
- name: backend
volumeMounts:
- name: data
mountPath: /mnt/data
volumes:
- name: data
persistentVolumeClaim:
claimName: storagelink-data-pvc
k8s/components/lfs/pvc.yaml
Only needed for Local File Storage.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: storagelink-data-pvc
spec:
# ReadWriteMany is required: the backend runs 2 replicas and every pod must
# see the same files. A ReadWriteOnce volume (e.g. Azure Disk / managed-csi)
# binds to a single node and AZ and will not work here.
accessModes:
- ReadWriteMany
# Azure Files. Preinstalled on AKS.
# Other clouds: AWS efs-sc, GCP standard-rwx.
storageClassName: azurefile
resources:
requests:
storage: 50Gi
k8s/components/openshift/kustomization.yaml
Only needed on OpenShift.
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
# OpenShift restricted-v2 SCC compatibility. The SCC rejects any fsGroup or
# runAsUser outside the namespace's preallocated range, so this removes every
# hard-coded ID and lets the SCC assign them. Removing only some of them still
# fails admission. Opt in on OpenShift only, by adding this to the top-level
# kustomization.yaml:
#
# components:
# - components/openshift
#
# Equivalent to the Helm chart's
# global.compatibility.openshift.adaptSecurityContext=force.
patches:
- target:
kind: Deployment
name: backend
patch: |-
- op: remove
path: /spec/template/spec/securityContext/fsGroup
- op: remove
path: /spec/template/spec/initContainers/0/securityContext/runAsUser
- op: remove
path: /spec/template/spec/containers/0/securityContext/runAsGroup
- target:
kind: Deployment
name: ui
patch: |-
- op: remove
path: /spec/template/spec/containers/0/securityContext/runAsUser
