Browser-based file access to AWS S3, Azure Blob/File Share, and Google Cloud Storage.
457
A security patch of 1.3.0. Every deployment should upgrade: replace the 1.3.0 images with 1.3.1 and keep the existing database.
LICENSE_CONTENT for unattended deploymentsrestricted pod security standard and support OpenShift arbitrary UIDs and readOnlyRootFilesystemFor the full changelog, see the CHANGELOG.
StorageLink gives your users secure, browser-based access to files in AWS S3, Azure Blob Storage, Azure File Share, Google Cloud Storage, and local file systems. Users upload, download, preview, and move files through a web interface, and files stream through your own StorageLink deployment to your storage, never through a third-party service.
Administrators manage users, folder permissions, storage connections, transfer jobs, and single sign-on (OIDC) from the same web interface.
StorageLink consists of two container images that work together:
| Image | Description |
|---|---|
| thorntech/storagelink-backend (this image) | REST API, cloud storage integration, and transfer jobs |
| thorntech/storagelink-ui | Web interface for users and administrators |
A PostgreSQL database is also required (included in the quick start below).
Run the following to generate security credentials and a self-signed TLS certificate, and save them to a .env file that Docker Compose picks up automatically:
# Generate security credentials
SECURITY_CLIENT_ID=$(openssl rand -hex 16)
SECURITY_CLIENT_SECRET=$(openssl rand -hex 32)
SECURITY_JWT_SECRET=$(openssl rand -hex 32)
# Generate a self-signed TLS certificate (valid for 1 year)
openssl req -x509 -newkey rsa:2048 -keyout tls.key -out tls.crt \
-days 365 -nodes -subj "/CN=localhost" 2>/dev/null
# Save everything to .env (Docker Compose reads this automatically)
{
echo "SECURITY_CLIENT_ID=${SECURITY_CLIENT_ID}"
echo "SECURITY_CLIENT_SECRET=${SECURITY_CLIENT_SECRET}"
echo "SECURITY_JWT_SECRET=${SECURITY_JWT_SECRET}"
printf 'WEBSITE_BUNDLE_CRT="%s"\n' "$(cat tls.crt)"
printf 'WEBSITE_KEY="%s"\n' "$(cat tls.key)"
} > .env
rm -f tls.crt tls.key
echo "Credentials saved to .env"
Create a docker-compose.yml file:
services:
db:
container_name: storagelink_db
image: postgres:18-alpine
environment:
POSTGRES_DB: swift_gw
POSTGRES_USER: swiftgw
POSTGRES_PASSWORD: swiftgw
PGDATA: /var/lib/postgresql/data/pgdata
POSTGRES_INITDB_ARGS: "--data-checksums"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U swiftgw -d swift_gw"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- storagelink-network
init-permissions:
image: thorntech/storagelink-backend:1.3.1
user: root
entrypoint: ["/bin/sh", "-c"]
# The backend runs as numeric UID 1000 / GID 0, so the local storage
# volume must be owned by that UID before the backend starts.
command: ["chown -R 1000:0 /mnt/storagelink_1"]
volumes:
- storage:/mnt/storagelink_1
backend:
container_name: storagelink_backend
image: thorntech/storagelink-backend:1.3.1
depends_on:
db:
condition: service_healthy
init-permissions:
condition: service_completed_successfully
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/swift_gw
SPRING_DATASOURCE_USERNAME: swiftgw
SPRING_DATASOURCE_PASSWORD: swiftgw
SECURITY_CLIENT_ID: ${SECURITY_CLIENT_ID}
SECURITY_CLIENT_SECRET: ${SECURITY_CLIENT_SECRET}
SECURITY_JWT_SECRET: ${SECURITY_JWT_SECRET}
FEATURES_FIRST_CONNECTION_CLOUD_PROVIDER: lfs
FEATURES_FIRST_CONNECTION_BASE_PREFIX: /mnt/storagelink_1
volumes:
- storage:/mnt/storagelink_1
# The backend is reached only through the UI container, so it needs no
# published ports.
user: "1000:0"
restart: unless-stopped
networks:
- storagelink-network
ui:
container_name: storagelink_ui
image: thorntech/storagelink-ui:1.3.1
depends_on:
- backend
environment:
BACKEND_URL: http://backend:8080/
SECURITY_CLIENT_ID: ${SECURITY_CLIENT_ID}
SECURITY_CLIENT_SECRET: ${SECURITY_CLIENT_SECRET}
WEBSITE_BUNDLE_CRT: ${WEBSITE_BUNDLE_CRT}
WEBSITE_KEY: ${WEBSITE_KEY}
# The UI image serves HTTP on 8080 and HTTPS on 8443 (non-privileged
# in-container ports); map the standard host ports onto them. Keep HTTPS on
# host port 443: the HTTP landing page links to https://<host> with no port.
ports:
- "80:8080"
- "443:8443"
restart: unless-stopped
networks:
- storagelink-network
volumes:
postgres_data:
driver: local
storage:
driver: local
networks:
storagelink-network:
driver: bridge
docker compose up -d
Open https://localhost and accept the self-signed certificate. While the backend is starting, the page shows a "server is starting" screen and loads the sign-in page automatically once the server is ready.
On first launch, a setup screen creates the initial administrator account. The quick start also creates a Local File System connection backed by the storage volume, ready for uploads and downloads as soon as a license is active (next step).
StorageLink requires a valid license before it transfers files. Until one is in place, administrators can sign in and manage settings, users, and licensing, but uploads, downloads, and transfer jobs are blocked for every role.
Sign in as the administrator. While the instance is unlicensed, a Start a 30-day free trial card appears:
The verification request is made by your browser, which needs to reach https://licensing.thorntech.com. The containers themselves do not need outbound internet access for this step. One trial is issued per email address.
If you already hold a license, whether a trial or a purchased one, there are two ways to install it. Use one or the other, not both.
Enter it in the dashboard. In Settings, find the License card, paste the license into the field or drop the license file onto it, and choose Activate. The license is stored in the database, so it survives container restarts and image upgrades, every instance in a clustered deployment picks it up, and it can be replaced later from the same card. This is the better choice for most deployments.
Inject it through the environment. Set LICENSE_CONTENT on the backend service, which suits immutable infrastructure and unattended deployments where nobody signs in to the dashboard:
backend:
environment:
LICENSE_CONTENT: ${LICENSE_CONTENT}
A license supplied this way that is not yet bound to a cluster is activated automatically at startup.
LICENSE_CONTENT takes precedence over the stored license on every license reload. If it is set, activating a different license in the dashboard appears to work and is then replaced by the environment value within a minute, so leave LICENSE_CONTENT unset whenever you intend to manage licensing from the dashboard.
| Variable | Description | Required |
|---|---|---|
SPRING_DATASOURCE_URL | PostgreSQL JDBC connection string | Yes |
SPRING_DATASOURCE_USERNAME | Database username | Yes |
SPRING_DATASOURCE_PASSWORD | Database password | Yes |
SECURITY_CLIENT_ID | OAuth client ID (must match the UI) | Yes |
SECURITY_CLIENT_SECRET | OAuth client secret (must match the UI) | Yes |
SECURITY_JWT_SECRET | JWT signing secret. If unset, a random value is generated at every start and all sessions end on restart. | Yes |
SERVER_PORT | API server port (default: 8080) | No |
FEATURES_FIRST_CONNECTION_CLOUD_PROVIDER | Storage connection created on first launch: lfs, aws, azure, azure_fileshare, or gcp (default: none) | No |
FEATURES_FIRST_CONNECTION_BASE_PREFIX | Storage location for that connection, such as a directory for lfs. No connection is created unless this is set. | No |
LICENSE_CONTENT | License contents. A license not yet bound to a cluster is activated automatically at startup. | No |
SECURITY_CLIENTIP_TRUSTEDPROXIES | Comma-separated CIDR ranges whose X-Forwarded-For headers are trusted when recording client addresses (default: loopback and private ranges) | No |
AWS_REGION | Default AWS region (default: us-east-1) | No |
JAVA_OPTS | Extra JVM options. The heap defaults to 80% of the container memory limit. | No |
See thorntech/storagelink-ui for the UI container's variables, TLS options, and ports.
| Port | Protocol | Description |
|---|---|---|
| 8080 | TCP | REST API, reached by the UI container at BACKEND_URL |
| Path | Description |
|---|---|
/mnt/storagelink_1 | Local File System storage in the quick start. Any path outside /opt/swiftgw works; the storage directory must be writable by UID 1000 or GID 0. |
/tmp | Upload chunks are spooled here during transfers. Under readOnlyRootFilesystem, mount a writable volume with room for the uploads in flight. |
| Path | Description |
|---|---|
/actuator/health/liveness | Liveness probe |
/actuator/health/readiness | Readiness probe, including the database check |
The backend writes structured JSON logs to standard output. Each line carries a logger_type field of application, audit, or license-audit, so a log collector can route the streams separately. No log files are written inside the container.
StorageLink ships and is tested against PostgreSQL 18 (the version in the quick start above). PostgreSQL 16 and 17 are also compatible, with no application or JDBC driver changes required.
StorageLink connects to:
Configure storage connections and their credentials through the admin dashboard.
StorageLink container images are built on Docker Hardened Images base images, run as a non-root user, and listen only on non-privileged ports. Every release includes an SBOM (Software Bill of Materials) and SLSA provenance attestations, so you can verify what an image contains and how it was built.
You can inspect the SBOM and provenance for any image using Docker Scout or BuildKit imagetools:
# View attestations with Docker Scout
docker scout attestation list thorntech/storagelink-backend:latest
# Inspect SBOM and provenance with buildx
docker buildx imagetools inspect thorntech/storagelink-backend:latest --format '{{json .SBOM}}'
docker buildx imagetools inspect thorntech/storagelink-backend:latest --format '{{json .Provenance}}'
Vulnerability scanners report three CRITICAL findings in the 1.3.0 backend image, all in its embedded Apache Tomcat 11.0.22: CVE-2026-65182, CVE-2026-65905, and CVE-2026-68525. None of them is reachable in StorageLink. Each one requires a Tomcat feature that StorageLink does not use:
| CVE | Requires |
|---|---|
| CVE-2026-65182 | Security constraints declared to Tomcat itself (<security-constraint>) |
| CVE-2026-65905 | Tomcat DIGEST authentication |
| CVE-2026-68525 | Tomcat FORM authentication |
StorageLink declares no Tomcat security constraints or authenticators. It authenticates and authorizes every request in the application instead. The Apache Tomcat project rates these issues Important, Low, and Low. StorageLink 1.3.1 upgrades Tomcat to 11.0.25, which clears all three.
StorageLink is commercial software. Use is governed by the End User License Agreement. A 30-day free trial is included, with no credit card required.
To purchase a license, visit storagelink.co/purchase or contact [email protected].
For technical support, contact [email protected].
StorageLink is developed by Thorn Technologies, specialists in secure file transfer and cloud integration solutions.
Content type
Image
Digest
sha256:519f1c6c8…
Size
572.2 MB
Last updated
2 days ago
docker pull thorntech/storagelink-backend