Azure Storage SFTP is Microsoft's managed file transfer service on top of Azure Blob Storage, convenient, but billed continuously per endpoint (roughly $0.30/hour, ~$220/month) and tied to Azure AD. SFTPGo is an open-source file transfer server offering SFTP, FTP/S, and WebDAV with pluggable storage backends (local disk, Azure Blob, S3-compatible, GCS) and no per-endpoint charge. This guide deploys SFTPGo with Docker Compose and Traefik, sets up user auth (password + SSH key + 2FA), connects S3-compatible object storage, and covers the migration path from Azure Storage SFTP. By the end, you'll have a self-hosted file transfer server with the same capabilities at zero endpoint cost.
Azure Storage SFTP → SFTPGo Mapping
| Azure Storage SFTP | SFTPGo Equivalent | Notes |
|---|---|---|
| SFTP Endpoint | SFTPGo SFTP Server | Configurable port, default 2022
|
| Azure Blob Storage | Azure Blob backend | Native support; point at the same container, no migration needed |
| Azure AD Authentication | LDAP/OIDC plugin | External identity provider via plugin |
| Local Users | Web UI / REST API user management | |
| Hierarchical Namespace | Virtual directories | No HNS requirement |
| Azure Monitor | Built-in logging + webhooks/syslog |
Prerequisite: Linux server with Docker + Compose, a DNS A record for your domain, and (if migrating) an existing Azure Storage account with SFTP enabled plus the Azure CLI installed locally.
Deploy with Docker Compose
1. Create the project directories:
$ mkdir -p ~/sftpgo/{data,config}
$ cd ~/sftpgo
2. Create the environment file:
$ nano .env
DOMAIN=sftp.example.com
LETSENCRYPT_EMAIL=admin@example.com
3. Create the Compose manifest:
$ nano docker-compose.yml
services:
traefik:
image: traefik:v3.6
container_name: traefik
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- "letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
restart: unless-stopped
sftpgo:
image: drakkan/sftpgo:v2.7
container_name: sftpgo
restart: unless-stopped
ports:
- "2022:2022"
- "2121:2121"
- "10080:10080"
environment:
- SFTPGO_HTTPD__BINDINGS__0__PORT=8080
- SFTPGO_HTTPD__BINDINGS__0__ADDRESS=0.0.0.0
- SFTPGO_SFTPD__BINDINGS__0__PORT=2022
- SFTPGO_FTPD__BINDINGS__0__PORT=2121
- SFTPGO_WEBDAVD__BINDINGS__0__PORT=10080
- SFTPGO_DATA_PROVIDER__DRIVER=sqlite
- SFTPGO_DATA_PROVIDER__NAME=/var/lib/sftpgo/sftpgo.db
volumes:
- ./data:/srv/sftpgo
- ./config:/var/lib/sftpgo
labels:
- "traefik.enable=true"
- "traefik.http.routers.sftpgo.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.sftpgo.entrypoints=websecure"
- "traefik.http.routers.sftpgo.tls.certresolver=letsencrypt"
- "traefik.http.services.sftpgo.loadbalancer.server.port=8080"
healthcheck:
test: ["CMD", "sftpgo", "ping", "-c", "/var/lib/sftpgo"]
interval: 30s
timeout: 10s
retries: 3
volumes:
letsencrypt:
Traefik terminates TLS on 80/443 for the admin UI; SFTP/FTP/S/WebDAV listen directly on 2022/2121/10080. Port 8080 (the admin UI's internal port) is never exposed to the host — Traefik proxies to it over the Docker network.
4. Start the stack:
$ docker compose up -d
$ docker compose ps
$ docker compose logs
Initial Admin Setup
- Open
https://sftp.example.com/web/admin. - Create the administrator: username, strong password, confirm, Create admin and Sign in.
- Review Server Manager → Configurations — check the SFTP, ACME, and SMTP sections match your requirements.
Create a User and Configure Authentication
1. Password auth: Users → Add — username, password, Status: Active, File system: Local disk, Root directory: /srv/sftpgo/USERNAME. Save.
2. SSH key auth (passwordless, mirrors Azure's SSH key support):
$ ssh-keygen -t ed25519 -f ~/.ssh/sftpgo_key -C "sftp-user"
$ cat ~/.ssh/sftpgo_key.pub
Paste the public key into Users → Edit → Public keys, then test:
$ sftp -i ~/.ssh/sftpgo_key -P 2022 USERNAME@sftp.example.com
3. 2FA: Users → Edit → ACLs → Require 2FA for — select the protocols to gate. The user sets up their authenticator app on next WebClient login.
Connect S3-Compatible Object Storage
SFTPGo supports Azure Blob directly, but also any S3-compatible provider — useful if you're consolidating storage away from Azure.
- Gather your endpoint URL, access key, secret key, and bucket name from the provider.
- Users → Edit → File system → Storage: S3 (Compatible).
- Fill in Bucket, Region, Access Key, Access Secret, Endpoint (full URL).
- Enable Use path-style addressing.
- Save, then verify:
$ echo "Hello SFTPGo" > testfile.txt
$ sftp -P 2022 USERNAME@sftp.example.com
sftp> put testfile.txt
sftp> ls
sftp> exit
S3 backend limitations: resume uploads disabled, no symlinks, non-atomic directory rename,
chmod/chownsilently ignored.
Test All Protocols
$ echo "Hello SFTPGo" > testfile.txt
$ sftp -P 2022 USERNAME@sftp.example.com
sftp> put testfile.txt
sftp> get testfile.txt /tmp/testfile-downloaded.txt
sftp> exit
$ sftp -i ~/.ssh/sftpgo_key -P 2022 USERNAME@sftp.example.com
$ sudo apt install lftp -y
$ lftp -u USERNAME -p 2121 ftp://sftp.example.com
lftp> ls
lftp> exit
Migrate from Azure Storage SFTP
Export Users
$ az storage account local-user list --account-name STORAGE_ACCOUNT --resource-group RESOURCE_GROUP
$ az storage account local-user show --account-name STORAGE_ACCOUNT --resource-group RESOURCE_GROUP --name USERNAME
Record username, SSH public keys, home directory (container + path), and permission scope for each user.
Recreate Users in SFTPGo
$ curl -u ADMIN_USERNAME:ADMIN_PASSWORD https://sftp.example.com/api/v2/token
Copy the access_token — it expires in 15 minutes.
$ curl -X POST "https://sftp.example.com/api/v2/users" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "USERNAME",
"password": "PASSWORD",
"status": 1,
"public_keys": ["SSH_PUBLIC_KEY"],
"home_dir": "/srv/sftpgo/data/USERNAME",
"permissions": {"/": ["*"]}
}'
"status": 1 is required — without it the account is created disabled.
Migrate Data
If SFTPGo will use Azure Blob as its own backend, just point users at the same container — no data transfer needed. For local-disk backends, pull the data down:
$ az storage blob download-batch --account-name STORAGE_ACCOUNT --source CONTAINER_NAME --destination ~/sftpgo/data/USERNAME/
$ sudo chown -R 1000:1000 ~/sftpgo/data/
Map Authentication Methods
| Azure Method | SFTPGo Equivalent |
|---|---|
| SSH Public Keys | Public Keys (direct) |
| Local User Passwords | Password Authentication |
| Azure AD (Preview) | LDAP/OIDC plugin, or external auth hook |
Update Clients
Document the new endpoint (sftp.example.com, port 2022, SSH key or password) and roll it out before decommissioning the Azure endpoint. Export SFTPGo's SSH host key (/var/lib/sftpgo/id_*) and share the fingerprint so clients don't hit host-key warnings.
Things to Watch
- Hierarchical Namespace: Azure Storage SFTP requires HNS; SFTPGo simulates directories without it — check any app that depends on HNS-specific behavior.
- Container-level permissions: map to SFTPGo's per-directory ACLs.
- Lifecycle policies: SFTPGo doesn't manage Azure Blob lifecycle rules — keep configuring those in Azure if you stay on the Blob backend.
- Azure Monitor: replace with SFTPGo's logging, syslog, or webhook integrations.
Next Steps
SFTPGo is running as a self-hosted Azure Storage SFTP replacement with zero per-endpoint charge. From here you can:
- Enable the Azure Blob backend directly if you want to keep data in place
- Set quotas, bandwidth limits, and IP ACLs per user under Edit → Disk quota / ACLs
- Wire event hooks (webhooks/syslog) to replace Azure Monitor alerting
For the full guide with additional tips, visit the original article on Vultr Docs.
Top comments (0)