Running the In-Place Upgrade Script to v3.10.0
TLDR - Quick Summary
What: In-place upgrade script to v3.10.0
Prerequisite: Must be on v3.6.0 or later
Run:
sudo su && wget https://thorntech-products.s3.amazonaws.com/sftpgateway/3.010.00/in-place-upgrade.sh && chmod +x in-place-upgrade.sh && ./in-place-upgrade.shRecommended: Create backup file first via Settings > Backup & Recovery
Overview
This article goes over how to run the in-place upgrade script to upgrade to version 3.010.00.
In order to run the in-place upgrade script you must be on version 3.006.00 or later due to the introduction of application license files in our build process. For more information on our updated plans & pricing introduced in version 3.6.0, check out this article.
Important Mentions
Make a new Backup File
Our recommended approach for upgrading to a new version of SFTP Gateway is to spin up a new SFTP Gateway instance and import your Users & Settings into the new instance using a Backup file. You can export a new Backup file under Settings ---> Backup & Recovery ---> Export Backup File. For more information on our recommended upgrade process, check out this article.
We've created this script for user conveniance, but the safest approach would be the method mentioned above. Before running the script, we would highly recommend creating a new Backup File containing your Users & Settings.
LetsEncrypt
Additionally, if you used LetsEncrypt to create an SSL cert, make sure to edit the new website.conf file to include your hostname, as the script will install a new website.conf file during the upgrade. The new website.conf file is located here:
/etc/nginx/sites-available/website.conf
Check out the Troubleshooting section of the LetsEncrypt article to see what the website.conf file should look like for you.
Memory Settings
The script also adds a new SFTP Gateway configuration file, which controls the Memory Settings for the SFTP Gateway application. The conf file is located at:
/opt/sftpgw/sftpgateway-admin-api-3.10.0.conf
By default, the configuration file is set to use the Memory Settings for a VM that has 8GB of RAM. If your VM has more or less than 8GB of RAM, make sure to edit your conf file to reflect your VM size.
You can use this article to find out the correct Memory values for your VM size.
Troubleshooting
If you run the script and you become stuck at the Please wait while SFTP Gateway finishes setting up loading screen, contact us at support@thorntech.com and send us the most recent application log (Date is included in the name):
/opt/sftpgw/log/application-2026-02-16.log
Running the Script
SSH into the VM and run this command to elevate your privileges:
sudo su
If you have run the in-place upgrade script before, make sure you're in a different directory than where you previously ran it. You can run this command to create a new directory and move into it:
mkdir sftpgw-3100-upgrade
cd sftpgw-3100-upgrade
Next, run a wget command to download the script:
wget https://thorntech-products.s3.amazonaws.com/sftpgateway/3.010.00/in-place-upgrade.sh
Give the script execute permissions:
chmod +x in-place-upgrade.sh
Finally, run the script:
./in-place-upgrade.sh
When you refresh your Web Admin UI you should now see an updated UI and version at the bottom.
Video Reference
Here are the contents of the script for reference:
#!/bin/bash
#
# SFTP Gateway in-place upgrade script — upgrades an existing 3.6.0+ instance to v3.10.0.
#
# How to run:
# cd ~
# curl -sS -O https://thorntech-products.s3.amazonaws.com/sftpgateway/3.010.00/in-place-upgrade.sh
# sudo bash ./in-place-upgrade.sh
#
# Run it with "sudo bash" from your home directory. /tmp is mounted noexec on
# the hardened appliance, so "sudo ./in-place-upgrade.sh" from /tmp fails with
# "Permission denied".
#
# Show debug output, and halt on errors
set -xe
# Must run script as root, or else show usage
if [[ $(whoami) != "root" ]]; then
echo "Usage: sudo bash $0"
exit 1
fi
LICENSE_DIR="/etc/thorn/"
# Proceed only if the instance is licensed (baked-in file license, or a BYOL-activated license).
function license_file_present {
[ -d "$LICENSE_DIR" ] && [ -n "$(ls -A "$LICENSE_DIR" 2>/dev/null)" ]
}
function license_in_database {
local count
count=$(sudo -u postgres psql -Xt -d sftpgw -c \
"SELECT count(*) FROM properties WHERE application='sftpgateway' AND key='license.content' AND value IS NOT NULL AND length(trim(value)) > 0;" \
2>/dev/null | tr -d '[:space:]')
[ "${count:-0}" -ge 1 ] 2>/dev/null
}
if license_file_present || license_in_database; then
echo "License found. Continuing..."
else
echo "Error: No license found. You must be on version 3.6.0 or later with an active license to run the in-place upgrade."
echo "Contact support@thorntech.com for help upgrading from the cloud marketplace, thanks!"
exit 1
fi
function extractPropValueFromSourceFile {
local prefix="${1}"
local str=`grep "${prefix}" ${2} 2>/dev/null`
echo "${str#$prefix}" | xargs
}
# If on version 2, exit script as the command sftpgw version is only on version 2.x
command -v sftpgw version >/dev/null && exit
# Set target version
TARGET_VERSION="3.10.0"
TARGET_VERBOSE_VERSION="3.010.00"
# Set date
TODAY=$(date +"%m%d%Y")
APPLICATION_PROPERTIES="/opt/sftpgw/application.properties"
# Determine the cloud provider
# head -c keeps a wrong-cloud 404 page (e.g. Google's HTML error page) from
# flooding the trace output — the real values are all short strings.
AWS_DOMAIN=$(curl -s --max-time 3 "http://169.254.169.254/latest/meta-data/services/domain" | head -c 40)
AZURE_DOMAIN=$(curl -s --noproxy "*" --max-time 3 -H 'Metadata: True' "http://169.254.169.254/metadata/instance/compute/azEnvironment?api-version=2019-06-01&format=text" | head -c 40)
GCP_CHECK=$(curl -s --max-time 2 "http://169.254.169.254/computeMetadata/v1/instance/zone" -H "Metadata-Flavor: Google" | head -c 80)
CLOUD_PROVIDER=aws
[[ -n $GCP_CHECK ]] && CLOUD_PROVIDER=gcp
[[ $AWS_DOMAIN == "amazonaws.com" ]] && CLOUD_PROVIDER=aws
[[ $AZURE_DOMAIN == "AzurePublicCloud" ]] && CLOUD_PROVIDER=azure
# Determine operating system and Nginx user
if getent passwd www-data > /dev/null 2>&1; then
OS=ubuntu
NGINX_USER=www-data
NGINX_CONF_PATH="/etc/nginx/sites-available"
else
OS=al2023
NGINX_USER=nginx
NGINX_CONF_PATH="/etc/nginx/conf.d"
fi
# Pre-flight: refuse to start if the CURRENT nginx config already fails
# validation. This upgrade swaps in a new website.conf and restarts nginx —
# doing that on top of a broken config would take the admin UI down.
if ! nginx -t; then
echo "" >&2
echo "ERROR: 'nginx -t' fails on this instance's CURRENT nginx configuration." >&2
echo "The upgrade has NOT started and nothing has been changed." >&2
echo "Fix the nginx configuration (run 'sudo nginx -t' to see the error), then re-run this script." >&2
exit 1
fi
function generate_password() {
local length=${1:-16}
echo -n "$(
head /dev/urandom | tr -dc A-Z0-9 | head -c $length
echo ''
)"
}
function set_jwt_secret_in_application_properties() {
local jwt_secret=${1}
echo "Remove any existing security.jwt.secret property"
sudo sed -i.bak '/^security\.jwt\.secret=/d' ${APPLICATION_PROPERTIES}
echo "Setting security.jwt.secret (value not logged)"
echo -e "security.jwt.secret=$jwt_secret" | sudo tee -a ${APPLICATION_PROPERTIES} >/dev/null
}
function synchronize_jwt_secret() {
local property1_key='jwt_secret'
local property1_value=$OAUTH_JWT_SECRET
# read -d '' always exits nonzero at heredoc EOF; without || true, set -e
# kills the script right here (this bit the published 3.9.0 script: the JWT
# sync and the postgres shell lockdown below it silently never ran).
read -r -d '' SQL_COMMAND <<EOF || true
WITH old AS (
SELECT
key,
value
FROM properties
WHERE application = 'sftpgateway'
AND profile = ''
AND label = ''
AND (key = '$property1_key')
),
new AS (
INSERT INTO properties (application, profile, label, key, value)
VALUES ('sftpgateway', '', '', '$property1_key', '${property1_value}')
ON CONFLICT DO NOTHING
RETURNING key, value
)
SELECT
key,
value
FROM new
UNION ALL
SELECT
key,
value
FROM old
order by key;
EOF
# Use `sudo -u postgres` (not `-i`): the postgres account may have a /sbin/nologin shell.
RESULTS=$(sudo -u postgres psql --command="$SQL_COMMAND" -Xt -d sftpgw)
SAVED_OAUTH_JWT_SECRET=$(echo "$RESULTS" | cut -d'|' -f 2 | xargs)
if [[ "$SAVED_OAUTH_JWT_SECRET" != "$OAUTH_JWT_SECRET" ]]; then
echo "Existing JWT Secret found in database"
set_jwt_secret_in_application_properties "$SAVED_OAUTH_JWT_SECRET"
sudo systemctl restart sftpgw-admin-api
fi
}
#
# Install the Java 21 runtime
#
# SFTP Gateway 3.10.0 runs on Java 21; instances upgrading from older versions ship Java 17,
# so install and activate Java 21 before starting the new jar.
if [ "$OS" == "ubuntu" ]; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y -q openjdk-21-jre-headless
update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java
else
yum install -y java-21-amazon-corretto
/usr/sbin/update-alternatives --set java /usr/lib/jvm/java-21-amazon-corretto.x86_64/bin/java
fi
# Halt (set -e) unless /usr/bin/java — the exact path the systemd unit execs —
# now resolves to Java 21. A wrong runtime dies at class load
# (UnsupportedClassVersionError) before the app's logging starts, so the only
# evidence would be in the systemd journal and the app log stays empty.
/usr/bin/java -version 2>&1 | grep -q 'version "21'
#
# Install SFTP Gateway files
#
# Download public resources (-O / -o keep this re-runnable after an aborted attempt)
# A pre-staged assets.zip next to the script (QA / air-gapped runs) is used as-is; otherwise download it.
if [ -s assets.zip ]; then
echo "Using the pre-staged assets.zip in $(pwd)"
else
wget -O assets.zip https://thorntech-products.s3.amazonaws.com/sftpgateway/${TARGET_VERBOSE_VERSION}/assets.zip
fi
unzip -o assets.zip
# Install Java files
# jar
cd assets
chmod +x sftpgateway-admin-api-${TARGET_VERSION}.jar
chown sftpgw:sftpgw sftpgateway-admin-api-${TARGET_VERSION}.jar
mv sftpgateway-admin-api-${TARGET_VERSION}.jar /opt/sftpgw/
# conf
chown sftpgw:sftpgw sftpgateway-admin-api-${TARGET_VERSION}.conf
mv sftpgateway-admin-api-${TARGET_VERSION}.conf /opt/sftpgw/
# Heap sizing: the .conf in assets.zip ships with JAVA_OPTS computed for the
# machine that built the release asset, not for this instance. Recompute
# -Xms/-Xmx from this instance's actual memory, matching what a fresh 3.10.0
# image computes at first boot (Single: ~15%/~48% of MemTotal; HA: ~22%/~73%,
# since HA offloads PostgreSQL to an external database).
ARCHITECTURE=$(grep -oE 'ARCHITECTURE=[A-Za-z]+' /opt/sftpgw/launch_config.env 2>/dev/null | tail -1 | cut -d= -f2 || true)
MEM_MB=$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)
if [ "${ARCHITECTURE}" == "HA" ]; then
XMS=$((MEM_MB * 22 / 100))
XMX=$((MEM_MB * 73 / 100))
else
XMS=$((MEM_MB * 15 / 100))
XMX=$((MEM_MB * 48 / 100))
fi
sed -i -E "s/-Xms[0-9]+[mg]/-Xms${XMS}m/; s/-Xmx[0-9]+[mg]/-Xmx${XMX}m/" /opt/sftpgw/sftpgateway-admin-api-${TARGET_VERSION}.conf
# Halt (set -e) if the heap settings didn't land
grep -q -- "-Xmx${XMX}m" /opt/sftpgw/sftpgateway-admin-api-${TARGET_VERSION}.conf
chown ${NGINX_USER}:${NGINX_USER} website.conf
mv ${NGINX_CONF_PATH}/website.conf ${NGINX_CONF_PATH}/website.conf-${TODAY}
mv website.conf ${NGINX_CONF_PATH}
# Validate the new nginx config BEFORE any services are stopped. On failure,
# put the previous (validated at pre-flight) config back and stop the upgrade.
if ! nginx -t; then
mv ${NGINX_CONF_PATH}/website.conf ${NGINX_CONF_PATH}/website.conf-${TARGET_VERSION}-failed
mv ${NGINX_CONF_PATH}/website.conf-${TODAY} ${NGINX_CONF_PATH}/website.conf
echo "" >&2
echo "ERROR: 'nginx -t' fails with the new website.conf in place (see the nginx output above)." >&2
echo "The previous nginx config has been restored; nginx and SFTP Gateway were NOT restarted." >&2
echo "The failing config was kept at ${NGINX_CONF_PATH}/website.conf-${TARGET_VERSION}-failed for inspection." >&2
if nginx -t; then
echo "The restored config validates cleanly." >&2
else
echo "WARNING: the restored config ALSO fails validation — fix the nginx configuration before doing anything else." >&2
fi
echo "Resolve the conflict, then re-run this script." >&2
exit 1
fi
# Set the OAuth JWT secret only now that the upgrade is past the nginx
# validation — an abort above must leave application.properties untouched.
# Trace output is suspended here: customers share these logs with support,
# and the JWT secret signs admin session tokens — it must never be logged.
{ set +x; } 2>/dev/null
OAUTH_JWT_SECRET=$(generate_password 128)
set_jwt_secret_in_application_properties "$OAUTH_JWT_SECRET"
set -x
# Install website files
mv admin-ui.tar.gz /usr/share/nginx
cd /usr/share/nginx
mv admin-ui admin-ui-${TODAY}
tar xzvpf admin-ui.tar.gz && rm -f $_
chown -R ${NGINX_USER}:${NGINX_USER} admin-ui
# Populate the application properties file. The OAuth client values are
# served publicly in webconfig.js by design, but keep them out of this log
# anyway — customers share these logs with support.
{ set +x; } 2>/dev/null
echo "Writing webconfig.js with this instance's OAuth client (values not logged)"
CLIENT_ID=$(extractPropValueFromSourceFile "security.client-id=" ${APPLICATION_PROPERTIES})
CLIENT_SECRET=$(extractPropValueFromSourceFile "security.client-secret=" ${APPLICATION_PROPERTIES})
(
cat <<EOF
window._env_ = {
"clientid": "$CLIENT_ID",
"clientsecret": "$CLIENT_SECRET",
"cloudProvider": "$CLOUD_PROVIDER",
"version": "$TARGET_VERSION"
};
EOF
) | sudo tee /usr/share/nginx/admin-ui/webconfig.js >/dev/null
cd admin-ui
chown -R ${NGINX_USER}:${NGINX_USER} webconfig.js
set -x
# Update the version
service sftpgw-admin-api stop
cd /etc/systemd/system/
cp -a sftpgw-admin-api.service sftpgw-admin-api.service-${TODAY}
sed -i "s/sftpgateway-admin-api-.*.jar/sftpgateway-admin-api-${TARGET_VERSION}.jar/" sftpgw-admin-api.service
# 3.10.0 launches via `java -jar` with an EnvironmentFile; older units exec the jar directly
# (a standard jar can't be exec'd -> "Exec format error"). Add the EnvironmentFile line and
# convert ExecStart to the java -jar form. Both guards make this idempotent.
grep -q "EnvironmentFile=.*sftpgateway-admin-api-${TARGET_VERSION}.conf" sftpgw-admin-api.service || \
sed -i "/^\[Service\]/a EnvironmentFile=-/opt/sftpgw/sftpgateway-admin-api-${TARGET_VERSION}.conf" sftpgw-admin-api.service
grep -qE "^ExecStart=/usr/bin/java " sftpgw-admin-api.service || \
sed -i -E 's#^ExecStart=(/[^ ]+\.jar)#ExecStart=/usr/bin/java $JAVA_OPTS -jar \1 $RUN_ARGS#' sftpgw-admin-api.service
# Pre-3.7 units don't pass the baked license to the app (the old app found
# /etc/thorn on its own; 3.10.0 only reads license.license-file-path). If a
# baked license file exists, the unit doesn't reference one, and no activated
# license lives in the database, wire the file in — otherwise 3.10.0 starts
# UNLICENSED ("No license was found from any source") and the admin UI offers
# a free trial on top of a paid instance.
LICENSE_FILE=$(ls -1t ${LICENSE_DIR} 2>/dev/null | head -1)
if [ -n "${LICENSE_FILE}" ] && ! license_in_database && \
! grep -q -- "--license.license-file-path" sftpgw-admin-api.service; then
sed -i "s#^ExecStart=.*#& --license.license-file-path=\"file:${LICENSE_DIR%/}/${LICENSE_FILE}\"#" sftpgw-admin-api.service
grep "^ExecStart=" sftpgw-admin-api.service
fi
# The 3.10.0 admin UI shows the version reported by the admin API (activeVersion
# from /backend/info) — the CRA-era static/js/main.*.js bundle this script used
# to stamp no longer exists in the 3.10.0 UI, so that step was removed.
sed -i "s/3.*/${TARGET_VERSION}/" /etc/profile.d/login-info.sh
# Restart Nginx (its config was already validated above; a failed restart halts the script)
service nginx restart
# Restart Java
systemctl daemon-reload
service sftpgw-admin-api start
# Same trace suspension as above: this handles the live JWT secret.
{ set +x; } 2>/dev/null
echo "Synchronizing JWT secret with the database (values not logged)"
synchronize_jwt_secret
set -x
sudo chsh -s /sbin/nologin postgres || echo ""