Deploy SFTP Gateway on Kubernetes
Overview
In this article we'll walk through deploying SFTP Gateway as a containerized application. This involves downloading zip files of our backend & admin-ui docker images, and loading them into your local docker image list.
From there, you will upload these images to your GCP Artifact Registry. A kubernetes manifest file will reference your Artifact Registry images, and deploy the SFTP Gateway containers.
Afterwards, you're able to access your deployment by navigating to your web browser and creating your initial Web Admin account.
Note: The SFTP Gateway container image is in beta. You are restricted to a single SFTP user, and the license on the beta container expires in a year. The purpose of this beta is for testing, and providing feedback which you can send to support@thorntech.com
.
Create local docker images
The first step involves downloading our docker image zip files in the form of an S3 link.
SFTP Gateway backend
image zip file:
https://sftpgateway-container-beta.s3.us-east-1.amazonaws.com/sftpgw-docker-images-version-3.8.0-snapshot.202508300318/sftpgateway-backend.tar.gz
SFTP Gateway admin-ui
image zip file:
https://sftpgateway-container-beta.s3.us-east-1.amazonaws.com/sftpgw-docker-images-version-3.8.0-snapshot.202508300318/sftpgateway-admin-ui.tar.gz
Once you've downloaded the zip files, using the command line/terminal, load them into your docker image list using the following commands:
# Load backend image
gzcat sftpgateway-backend.tar.gz | docker load
# Load admin UI image
gzcat sftpgateway-admin-ui.tar.gz | docker load
Verify that these images are pulled into your local docker:
docker image list
Push these containers to GCP Artifact Registry
If you don't have one already, create a new Artifact Registry in GCP.
Next, tag the images with the Artifact Registry destination:
docker tag 92b1fff4c636 us-east1-docker.pkg.dev/sftp-gateway/rob-sftpgw-test/sftpgateway-backend:v1.1
docker tag 7e5001fa87b9 us-east1-docker.pkg.dev/sftp-gateway/rob-sftpgw-test/sftpgateway-admin-ui:v1.1
The purpose of the tag is to determine the destination where the image gets pushed. Kind of like an address label.
Here is a breakdown of the commands:
docker tag
: You are apply a tag to the image92b1fff4c636
: This is the Image ID, which you can get from thedocker image list
command.us-east1
: This is the region of the Artifact Registrysftp-gateway
: This is the GCP project. Replace this with your own project.rob-sftpgw-test
: This is the name of the Artifact Registrysftpgateway-backend
: This is the name of the Package within the Artifact Registryv1.1
: This is the tag for the image version
After you have tagged these images, push them to your Artifact Registry.
docker push us-east1-docker.pkg.dev/sftp-gateway/rob-sftpgw-test/sftpgateway-backend:v1.1
docker push us-east1-docker.pkg.dev/sftp-gateway/rob-sftpgw-test/sftpgateway-admin-ui:v1.1
Apply the Kubernetes manifest
You may already have your own kubernetes cluster.
But in this basic example, I’m using autopilot. Here is the command used to deploy it:
gcloud container clusters create-auto rob-cluster --region=us-east1
On your local workstation (or in the GCP cloud shell), create the following file:
kubernetes-manifest.yaml
(See the contents of this file at the bottom of this document)
You will need to make the following changes to the file:
- On line
158
:image: us-east1-docker.pkg.dev/sftp-gateway/rob-sftpgw-test/sftpgateway-backend:v1.1
- On line
336
:image: us-east1-docker.pkg.dev/sftp-gateway/rob-sftpgw-test/sftpgateway-admin-ui:v1.1
Make sure you are pointing to the image in your own Artifact Registry.
Optional: Make changes to these other values:
- On line
58
: ThePOSTGRES_PASSWORD
is hard-coded tosftpgw
. You want to use a more secure value. - On line
171
: TheSPRING_DATASOURCE_PASSWORD
is hard-coded tosftpgw
. Set this to the same secure value you set above. - Consider changing the
SECURITY_CLIENT_ID
andSECURITY_CLIENT_SECRET
values - Consider changing the
website.crt
andwebsite.key
with your own SSL certificate
Once you have made the appropriate changes, deploy the kubernetes manifest file:
kubectl apply -f kubernetes-manifest.yaml
What did I just deploy
The kubernetes manifest file deploys the following resources:
Starting with the bottom of the diagram, there is are 3 pods that are deployed (shown in purple):
sftpgw-ui
: This is the web admin portal running on a web server.sftpgw-backend
: This is the SFTP server, running on a Java backendpostgres-db
: This is the postgres database
These pods are abstracted away in service layers (shown in blue):
sftpgw-ui
sftpgw-backend
db
For the services that are user-facing, there are load balancers you can connect to (shown in pink):
sftpgw-ui
sftpgw-backend-lb
Run the following command to get a list of pods:
kubectl get pods -n sftpgw
NAME READY STATUS RESTARTS AGE
postgres-db-5b7b7446b9-tk76l 1/1 Running 0 21m
sftpgw-backend-79fdc45c97-grr8b 1/1 Running 0 21m
sftpgw-ui-76d6d557c7-bvkl7 1/1 Running 0 21m
How to connect to the web admin portal
Run the following command to get a list of services:
kubectl get service -n sftpgw
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
db ClusterIP 34.118.232.175 <none> 5432/TCP 3h26m
db-external NodePort 34.118.239.114 <none> 5432:30455/TCP 3h26m
sftpgw-backend ClusterIP 34.118.231.200 <none> 8080/TCP,22/TCP 3h26m
sftpgw-backend-external NodePort 34.118.228.15 <none> 8080:30080/TCP,22:30244/TCP 3h26m
sftpgw-backend-lb LoadBalancer 34.118.226.57 34.139.141.133 22:31327/TCP 3h26m
sftpgw-ui LoadBalancer 34.118.234.34 34.74.250.188 80:31773/TCP,443:30730/TCP 3h26m
In the above output, make special note of the EXTERNAL-IP
column:
sftpgw-ui
: This is the public IP for connecting to the web admin portal. In this example, it’s34.74.250.188
sftpgw-backend-lb
: This is the public IP for connecting to the SFTP service. In this example, it’s34.139.141.133
Paste the sftpgw-ui
EXTERNAL-IP
(e.g. 34.74.250.188
) into your web browser.
You should see the SFTP Gateway web admin portal.
In order to test this beta, make the following changes:
- Create a web admin user and password
- Log in as this web admin user
- Go to the Settings tab and create a new Cloud Connection to a GCS bucket
- Go to the Folders tab and point the
root
folder to the Cloud Connection you just created - Go to the Users tab and create a new SFTP user
How to connect to the SFTP service
The SFTP service is hosted on the Java backend.
And in order to connect to the SFTP service, you will need to go through the load balancer.
In the above list of services, find the sftpgw-backend-lb
and look at the EXTERNAL-IP
. In my example, it’s 34.139.141.133
You can connect to this load balancer public IP on port 22:
sftp robtest@34.139.141.133
You should be prompted for a server fingerprint, and then connect via SFTP if your credentials are correct.
The kubernetes manifest file
# Namespace
apiVersion: v1
kind: Namespace
metadata:
name: sftpgw
---
# PostgreSQL PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data-pvc
namespace: sftpgw
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
# SFTP Home PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sftpgw-home-pvc
namespace: sftpgw
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
# PostgreSQL Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres-db
namespace: sftpgw
spec:
replicas: 1
selector:
matchLabels:
app: postgres-db
template:
metadata:
labels:
app: postgres-db
spec:
containers:
- name: postgres
image: postgres:16-alpine
env:
- name: POSTGRES_DB
value: "sftpgw"
- name: POSTGRES_USER
value: "sftpgw"
- name: POSTGRES_PASSWORD
value: "sftpgw"
- name: PGDATA
value: "/var/lib/postgresql/data/pgdata"
- name: POSTGRES_INITDB_ARGS
value: "--data-checksums"
- name: POSTGRES_LOGGING
value: "on"
ports:
- containerPort: 5432
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
livenessProbe:
exec:
command:
- pg_isready
- -U
- sftpgw
- -d
- sftpgw
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- pg_isready
- -U
- sftpgw
- -d
- sftpgw
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 5
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-data-pvc
restartPolicy: Always
---
# PostgreSQL Service
apiVersion: v1
kind: Service
metadata:
name: db
namespace: sftpgw
spec:
selector:
app: postgres-db
ports:
- port: 5432
targetPort: 5432
type: ClusterIP
---
# PostgreSQL NodePort Service (for external access like port 5455)
apiVersion: v1
kind: Service
metadata:
name: db-external
namespace: sftpgw
spec:
selector:
app: postgres-db
ports:
- port: 5432
targetPort: 5432
nodePort: 30455
type: NodePort
---
# SFTP Gateway Backend Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: sftpgw-backend
namespace: sftpgw
spec:
replicas: 1
selector:
matchLabels:
app: sftpgw-backend
template:
metadata:
labels:
app: sftpgw-backend
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: sftpgw-backend
image: us-east1-docker.pkg.dev/sftp-gateway/rob-sftpgw-test/sftpgateway-backend:v1.1
env:
- name: SECURITY_CLIENT_ID
value: "1234"
- name: SECURITY_CLIENT_SECRET
value: "1234"
- name: SECURITY_JWT_SECRET
value: "b03c367c-a16c-4a43-9d47-8ff73462179e"
- name: SPRING_DATASOURCE_URL
value: "jdbc:postgresql://db:5432/sftpgw"
- name: SPRING_DATASOURCE_USERNAME
value: "sftpgw"
- name: SPRING_DATASOURCE_PASSWORD
value: "sftpgw"
- name: SFTP_HOST_ADDRESS
value: "0.0.0.0"
- name: SERVER_PORT
value: "8080"
- name: AWS_REGION
value: "us-east-1"
- name: SPRING_PROFILES_ACTIVE
value: "local"
- name: LOGGING_LEVEL_ROOT
value: "INFO"
- name: LOGGING_LEVEL_COM_THORNTECH
value: "DEBUG"
ports:
- containerPort: 8080
- containerPort: 22
volumeMounts:
- name: sftpgw-home
mountPath: /home/sftpgw
workingDir: /opt/sftpgw
command: ["java", "-jar", "application.jar"]
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
volumes:
- name: sftpgw-home
persistentVolumeClaim:
claimName: sftpgw-home-pvc
restartPolicy: Always
---
# SFTP Gateway Backend Service
apiVersion: v1
kind: Service
metadata:
name: sftpgw-backend
namespace: sftpgw
spec:
selector:
app: sftpgw-backend
ports:
- name: http
port: 8080
targetPort: 8080
- name: sftp
port: 22
targetPort: 22
type: ClusterIP
---
# SFTP Gateway Backend External Service
apiVersion: v1
kind: Service
metadata:
name: sftpgw-backend-external
namespace: sftpgw
spec:
selector:
app: sftpgw-backend
ports:
- name: http
port: 8080
targetPort: 8080
nodePort: 30080
- name: sftp
port: 22
targetPort: 22
nodePort: 30244
type: NodePort
---
# ConfigMap for UI certificates
apiVersion: v1
kind: ConfigMap
metadata:
name: ui-certs
namespace: sftpgw
data:
website.crt: |
-----BEGIN CERTIFICATE-----
MIID4zCCAsugAwIBAgIUd2Q6F9G4dGxIqLd8cS6ltLeDpk4wDQYJKoZIhvcNAQEL
BQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM
DVNhbiBGcmFuY2lzY28xFDASBgNVBAoMC0RldmVsb3BtZW50MRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMjQxMTE3MDMwMzE4WhcNMjUxMTE3MDMwMzE4WjBkMQswCQYD
VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j
aXNjbzEUMBIGA1UECgwLRGV2ZWxvcG1lbnQxEjAQBgNVBAMMCWxvY2FsaG9zdDCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALY7NVKDdmGj0FLUY/mBjyvn
KQiF22bNxq56HTIKawjHxV70W+CBzPv2d2wNI4e4vOqTDCKOUbPKKPXX1vNu2u1l
McVQNwxR2xG4zsDj9WzX3Z782F6qmHi+hqsVbFgVdOxj4xjMOTeodwjNL2K4ylMi
Vn13M7Yj1qqPfXVRZgfcSuVmLpIadz5TAif4NhB6SBodg+jjeynGJLkS80kV13W9
bpu1JNcTFEaEB+E0RijdZ2BrUUaOaNF8wOP0Zk50v5t0M9uBfd9O0sDemzxesyWF
+pLANFkEPYiRCdn7epF1qZZer2uqbS15RhFyvfKfkZK0vx4vC/qcDKpI9CPg6t0C
AwEAAaOBjDCBiTAdBgNVHQ4EFgQUc31/aqt6ZE22MrD4JmpNZP8UNZswHwYDVR0j
BBgwFoAUc31/aqt6ZE22MrD4JmpNZP8UNZswDwYDVR0TAQH/BAUwAwEB/zAUBgNV
HREEDTALgglsb2NhbGhvc3QwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUF
BwMBMA0GCSqGSIb3DQEBCwUAA4IBAQAGtPbCCjw3ckDncp3UD32X9dfDLWZnvXRi
jsZ0kpLu3O1A+AJb7HGdeV7TB13DU6DvlHfPMExI8LPaybiLbAB5yO3pIYueoacc
y6QK3s8AbtubWMROOeICBe6WVh63nzoPk3LjgyDk16Nm84VQequIGGJ3PlXZzXjZ
PVlEEPryg8s6U8iY7z/HHXse4CytMrEB9last5Z3TBgNqTT5Uiqw9zAhb4L6oHwW
zE99hgVx5G5eAVeziPY5Hy1xYY7/G6EXmdpRWI5BbJO4kKN0RFS03b8iGkgiwHvp
24Yi6PfMCT1WSNQ9OEIjsAaQIKJikH1xww46y0oEn/wc2BBCgcOO
-----END CERTIFICATE-----
website.key: |
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC2OzVSg3Zho9BS
1GP5gY8r5ykIhdtmzcaueh0yCmsIx8Ve9Fvggcz79ndsDSOHuLzqkwwijlGzyij1
19bzbtrtZTHFUDcMUdsRuM7A4/Vs192e/Nheqph4voarFWxYFXTsY+MYzDk3qHcI
zS9iuMpTIlZ9dzO2I9aqj311UWYH3ErlZi6SGnc+UwIn+DYQekgaHYPo43spxiS5
EvNJFdd1vW6btSTXExRGhAfhNEYo3Wdga1FGjmjRfMDj9GZOdL+bdDPbgX3fTtLA
3ps8XrMlhfqSwDRZBD2IkQnZ+3qRdamWXq9rqm0teUYRcr3yn5GStL8eLwv6nAyq
SPQj4OrdAgMBAAECggEAJUl2puhO1fo4o2YhgblUlAFj2EJZaw7iXyuN40IV9hE4
Ta5WyViN2qVq+KE0mq3+e8n0UvLHfW/5UxpjuWI+qhIJbcv0w5DRMC5eIcJTIr8F
siUe2bny4kvrzsBer6ROTRtAKcAJ2h1eo96mGj9g6MNPKrN0EYoCP9qF63YpGCTO
zVpY7fIOxow2AHaiRlEblpRtJHMEL/DdcmxqvCvkREgjwQSWZKAL8Mu4dZKasYBy
+nNV7vIudGmTciRuWJLgLayLXFil7OJME44PIktGlBNL3UgMM/oKCTM0qp8aA9DQ
52dKWk9gI9I6mum+XnBMipQtBSFMyBHEoSb9dUytkQKBgQDonUUiz417NgO0fljE
0GvrqNzk2RrJ50KMcLB6wdJoVx037qLJkHJJUeKsJeOqJfYOQ68PnusucEvSgR5N
/69c1v5UfFASj+mVVtLaEsEEZ+MMSvYhSHq/Dlj4NdKaSkdcCxY8YoMN3fRNUTtY
o3oOQWDn+43uqe1Wabr2TMxUzQKBgQDIjT5jNo9H27bkiXUV2t+a9KmGTu2FNE9F
CtMrSAmpj+lCHPNJzwgP49KfGf+KerkZ/bslixhROXo0HXQ0Eyp+iduNmXKjEX/h
rpj+HekN/cdHyQA64Kc8qHYbN+4VgksXk1hWXovhY99bg2bEtzX9D4mFbmdArGXA
4eMNZfJuUQKBgACfAuM/6KHOmB3wRG5qHA+qCMT3q3Gkk3Hqjx4UzGoI6YQPuBGP
uC5n8JIDG+OFbG3HUn1ZMEmUdS9ftuQAbchyroUtO82A4t/KNo/sguVvHZUX0iZu
mh1OfYBULHbLAfvF785DeRQdZpyaPe1TqmzFUevsqQldHMwhRiWIOPd1AoGAYy6P
Cwvhgj0jzxQ3xm4vJXgYGqcQCk9bYJ7A3mfK94OHbT3aB8eOiiU2dZ6q5TZaMoNs
OV330bumNv3WCSbtXhUZcobPzduKrfbmDM6IAnZeRp8eMQAHVRVPC5j2csa0El25
U0WA0h/NR3nNqj2dQqCbd1SpVa+sxt4vpuGjKnECgYASiNxDdbApD6F153Y56dO1
PM17ujQ+sa0qZH+fdCKweBZ5xHN+I7VYf4zMJ630/7KWy9lwa4N8GWxNNGAEhWav
7xqWNw2UCfb5BzsztIxSL60NSnqCzqvZP7OSboFS9X6lQKQ73bWyAtQuhG5ydALz
0Bz/2SdeU40IbTl/wrpY2g==
-----END PRIVATE KEY-----
---
# SFTP Gateway UI Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: sftpgw-ui
namespace: sftpgw
spec:
replicas: 1
selector:
matchLabels:
app: sftpgw-ui
template:
metadata:
labels:
app: sftpgw-ui
spec:
containers:
- name: sftpgw-ui
image: us-east1-docker.pkg.dev/sftp-gateway/rob-sftpgw-test/sftpgateway-admin-ui:v1.1
env:
- name: BACKEND_URL
value: "http://sftpgw-backend:8080/"
- name: SECURITY_CLIENT_ID
value: "1234"
- name: SECURITY_CLIENT_SECRET
value: "1234"
- name: CLOUD_PROVIDER
value: "aws"
- name: WEBSITE_BUNDLE_CRT
valueFrom:
configMapKeyRef:
name: ui-certs
key: website.crt
- name: WEBSITE_KEY
valueFrom:
configMapKeyRef:
name: ui-certs
key: website.key
ports:
- containerPort: 80
- containerPort: 443
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
restartPolicy: Always
---
# SFTP Gateway UI Service
apiVersion: v1
kind: Service
metadata:
name: sftpgw-ui
namespace: sftpgw
spec:
selector:
app: sftpgw-ui
ports:
- name: http
port: 80
targetPort: 80
- name: https
port: 443
targetPort: 443
type: LoadBalancer
---
# Trying to reach the SFTP service
apiVersion: v1
kind: Service
metadata:
name: sftpgw-backend-lb
namespace: sftpgw
spec:
selector:
app: sftpgw-backend
ports:
- name: sftp
port: 22
targetPort: 22
protocol: TCP
type: LoadBalancer