What it does for you
SFTPGo gives each person you work with an account and a private folder. They connect with the SFTP program they already use, such as WinSCP or FileZilla, or sign in to its WebClient in a browser, where they can also make share links with a password, an expiry date or a download limit. You manage everyone from the WebAdmin pages. A folder can sit on the server's own disks, be encrypted at rest, or live in a cloud bucket (S3-compatible, Google Cloud or Azure Blob) or on another SFTP server.
Out of the box only SFTP is on, on port 2022; FTP and WebDAV have port 0 in sftpgo.json, which means off, and the project tells you to prefer SFTP. The image runs as user and group 1000, keeping accounts, settings and SSH host keys in a small SQLite database and key files under /var/lib/sftpgo.
Is the licence a problem for you?
For running it for your own business, usually not. The free Community edition carries the AGPL-3.0-only licence, and its NOTICE file attaches extra terms. What follows is how we read it, not legal advice: give someone a copy, changed or not, and they get the source on the same terms; change it and let people use your version across the internet, and you owe those people the changed source.
NOTICE goes further for one case: when SFTPGo becomes part of a service you offer and your own front end configures it, then even an unchanged copy needs clear credit to the project, nothing implying its endorsement, and a link where users can get the source. The README adds that the theme behind WebAdmin and WebClient is proprietary: it may be used only within SFTPGo, and reused in derivative works only if the SFTPGo team allows it. The authors also sell an Enterprise edition on a commercial licence.
The server that fits
What decides the box is how much your clients send. Our storage dedicated servers let you choose the SSD drives and the RAID layout, and a storage consultant on chat can help pick the RAID level. Blades and rack servers get hardware RAID; the m510/m710x/m750 cartridges get software RAID. Each machine has its own unshared port at the access switch and nothing meters the traffic, so a client sending a year of footage is billed exactly like a quiet week.
You get root, one IPv4 address, Ubuntu up to 26.04, and a console that works without the operating system on blades, rack servers and the m510/m710x cartridges. Phone and live chat are staffed 24/7, under a service level agreement. A racked server usually reaches you about 30 minutes after we approve the order; a build is 4 to 24 hours when the parts are stock we hold, 5 to 10 working days when they have to be bought in, and 10 to 15 for a scarce part. Other shapes: custom servers. Data that must stay in Europe: Amsterdam and Bucharest.
Doors you will open, and why
| Port on the server | What answers | Open to |
|---|---|---|
| 2022 | SFTP | Everyone you invite |
| 8080 on 127.0.0.1 only | WebAdmin and WebClient, plain HTTP | You, through an SSH tunnel |
| 80 | Certificate checks from Let's Encrypt | Let's Encrypt; leave it open, because each renewal is checked here |
| 443 | WebClient over HTTPS; admin pages and REST API switched off | Everyone you invite |
| 10443 (optional) | WebDAV over HTTPS | Only if you turn it on |
| 2121 and 50000-50100 (optional) | FTPS control and passive data | Only if you turn it on |
Docker publishes a port with no address on all of the server's IPv4 and IPv6 addresses, and on IPv4 it skips ufw entirely. On IPv6 ufw keeps filtering unless IPv6 is enabled inside Docker itself, and the rules here are IPv4-only on purpose: until then, IPv6 visitors would all arrive from one internal Docker address, and SFTPGo's lock-out for failed sign-ins would block them as a group. Switch Docker's IPv6 on for this compose network before opening SFTPGo to IPv6.
Up and running
Starting point: Ubuntu 24.04, plus Docker's own packages for Engine and Compose. Become root (sudo -i) and paste this. It stops without changing anything if Docker is older than 28 (older releases let a neighbouring machine on the local segment through to ports bound to 127.0.0.1; see moby#45610) or if an earlier setup exists.
S=/opt/sftpgo-docker
ver=$(docker version --format '{{.Server.Version}}' 2>/dev/null)
if ! [ "${ver%%.*}" -ge 28 ] 2>/dev/null; then echo "STOP: install Docker Engine 28 or newer first"
elif [ -e $S/settings.env ]; then echo "STOP: $S is already set up, nothing changed"
else
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw && ufw allow OpenSSH && ufw --force enable &&
ufw allow proto tcp to 0.0.0.0/0 port 2022 && install -d -m 755 $S &&
install -d -o 1000 -g 1000 -m 750 $S/state /srv/sftpgo /srv/sftpgo/data &&
install -d -o 1000 -g 1000 -m 700 /srv/sftpgo/backups &&
install -m 600 /dev/null $S/settings.env &&
echo SFTPGO_COMMON__DEFENDER__ENABLED=true > $S/settings.env &&
cat > $S/compose.yaml <<'EOF' && docker compose -f $S/compose.yaml up -d && echo started
services:
sftpgo:
image: drakkan/sftpgo:v2.7.6
container_name: sftpgo
restart: unless-stopped
env_file: /opt/sftpgo-docker/settings.env
ports:
- "2022:2022"
- "127.0.0.1:8080:8080"
- "80:8081"
- "443:8443"
volumes:
- /srv/sftpgo:/srv/sftpgo
- /opt/sftpgo-docker/state:/var/lib/sftpgo
EOF
fi
How this differs from the docker run example in the Docker docs, all of it: a compose file; release v2.7.6 pinned; a name and a restart policy; host folders writable by UID 1000, as those docs ask; 127.0.0.1 for the web port; 80 and 443 mapped with nothing behind them until the certificate step; and the defender, off by default, switched on to block addresses that keep failing to sign in.
Make yourself the admin, privately
SFTPGo's docs warn that until an admin exists, its setup screen lets whoever reaches it become one. Nobody else can reach yours yet, so do it now. Take care with your password here: tunnel sign-ins all reach SFTPGo from one internal Docker address, so a string of wrong attempts can shut the tunnel out for half an hour, the default ban time.
- From your laptop, open the tunnel:
ssh -N -L 38080:127.0.0.1:8080 you@files-boxand keep that window open. - Visit
http://127.0.0.1:38080/web/adminand create your admin. SFTPGo then offers two-factor sign-in; turn it on. - Add your first client under Users with a password or an SSH key. Their files go in a folder named after them inside
/srv/sftpgo/data. - Try it from anywhere:
sftp -P 2022 clientname@files-box, upload one file, download it again.
Give it a real certificate
Give your clients' hostname an A record that points here, fill in HOST and MAIL, and paste as root. It opens 80 and 443 first, then does the one-time sftpgo acme run that SFTPGo's docs ask for to get the first certificate; calling it via docker exec inside the running container is our own adaptation. Once the file exists it adds the public HTTPS listener, with the admin pages and REST API off there. Clients lose nothing: the WebClient's pages, uploads and shares use their own /web/client/ addresses (internal/httpd/httpd.go). The env file is read when the container is created, which --force-recreate guarantees.
HOST=files.example.com; MAIL=you@example.com
S=/opt/sftpgo-docker; F=$S/settings.env
if [ "$HOST" = files.example.com ] || [ "$MAIL" = you@example.com ]; then
echo "STOP: put your own name and email in HOST and MAIL"
elif ! getent ahostsv4 "$HOST" >/dev/null; then echo "STOP: $HOST has no A record yet"
else
ufw allow proto tcp to 0.0.0.0/0 port 80,443 &&
{ grep -q '^SFTPGO_ACME__DOMAINS=' $F || printf '%s\n' "SFTPGO_ACME__DOMAINS=$HOST" \
"SFTPGO_ACME__EMAIL=$MAIL" SFTPGO_ACME__CERTS_PATH=/var/lib/sftpgo/certs \
SFTPGO_ACME__HTTP01_CHALLENGE__PORT=8081 >> $F; } &&
docker compose -f $S/compose.yaml up -d --force-recreate && sleep 5 &&
docker exec sftpgo sftpgo acme run && test -s $S/state/certs/$HOST.crt &&
{ grep -q '^SFTPGO_HTTPD__BINDINGS__1__' $F || printf '%s\n' \
SFTPGO_HTTPD__BINDINGS__1__PORT=8443 SFTPGO_HTTPD__BINDINGS__1__ENABLE_HTTPS=true \
SFTPGO_HTTPD__BINDINGS__1__ENABLE_WEB_ADMIN=false SFTPGO_HTTPD__BINDINGS__1__ENABLE_REST_API=false \
"SFTPGO_HTTPD__BINDINGS__1__CERTIFICATE_FILE=/var/lib/sftpgo/certs/$HOST.crt" \
"SFTPGO_HTTPD__BINDINGS__1__CERTIFICATE_KEY_FILE=/var/lib/sftpgo/certs/$HOST.key" >> $F; } &&
docker compose -f $S/compose.yaml up -d --force-recreate && echo "ok: https://$HOST/web/client"
fi
Host port 80 leads to the challenge listener on 8081 inside the container; the docs expect the check on port 80 and accept a forward from there. After the first certificate you do nothing: the service checks twice a day, renews 30 days ahead through that same port 80 (so never close it), and reloads it for the web, FTP and WebDAV listeners (internal/acme/acme.go). Up to ten listeners per service can be set from variables (internal/config/config.go).
If someone insists on WebDAV or FTP
Both reuse the certificate. Each one is a few lines in /opt/sftpgo-docker/settings.env plus port mappings in the compose file; afterwards, inside /opt/sftpgo-docker, recreate the container with docker compose up --detach --force-recreate sftpgo.
- WebDAV: after the prefix
SFTPGO_WEBDAVD__BINDINGS__0__, setPORT=10443,ENABLE_HTTPS=true, thenCERTIFICATE_FILEandCERTIFICATE_KEY_FILEto the.crtand.keyin/var/lib/sftpgo/certs. Map"10443:10443";ufw allow proto tcp to 0.0.0.0/0 port 10443. - FTPS: after the prefix
SFTPGO_FTPD__BINDINGS__0__, setPORT=2121,TLS_MODE=1so plain FTP is refused on both connections, the same certificate pair, andFORCE_PASSIVE_IPset to your public IPv4. Docker puts SFTPGo behind NAT, and the docs say a server behind NAT needs that setting. - Passive transfers use ports 50000 to 50100, moved with
STARTandENDunderSFTPGO_FTPD__PASSIVE_PORT_RANGE__. Map"50000-50100:50000-50100"plus"2121:2121", then open them withufw allow proto tcp to 0.0.0.0/0 port 50000:50100,2121; keep the setting, the mapping and the rule in step if you move the range.
Keep a copy you can bring back
Your clients' files are in /srv/sftpgo; everything else (compose file, settings, database, SSH host keys, certificates) is in the small /opt/sftpgo-docker. The first lines archive that folder while SFTPGo is briefly stopped, so the database is copied whole, with the second in each name. Send /srv/sftpgo and /root/sftpgo-copies off the server with rsync -a, never --delete, or an emptied folder would empty your copy too.
# Take a copy (as root). SFTPGo pauses while tar runs.
S=/opt/sftpgo-docker; K=/root/sftpgo-copies; install -d -m 700 $K
f=$K/state-$(date +%F-%H%M%S).tar.gz
if docker compose -f $S/compose.yaml stop; then tar -czf $f -C / opt/sftpgo-docker; rc=$?
docker compose -f $S/compose.yaml start
if [ $rc -eq 0 ] && tar -tzf $f >/dev/null; then echo "ok $f"; else echo "STOP - copy did not complete, delete $f"; fi
else echo "STOP - SFTPGo would not stop, no copy taken"; fi
# Bring one back (as root, also on a brand-new server). Name the real file in PICK.
S=/opt/sftpgo-docker; PICK=/root/sftpgo-copies/state-YYYY-MM-DD-HHMMSS.tar.gz
if ! { [ -s "$PICK" ] && tar -tzf "$PICK" >/dev/null; }; then echo "STOP - $PICK is missing or damaged"
elif [ -e $S.previous ]; then echo "STOP - $S.previous exists; deal with it first"
else
if [ -e $S/compose.yaml ]; then docker compose -f $S/compose.yaml down && mv $S $S.previous; fi &&
tar -xzf "$PICK" -C / && chown -R 1000:1000 $S/state && chmod 600 $S/settings.env &&
docker compose -f $S/compose.yaml up -d && echo "back; any replaced state is in $S.previous"
fi
# Undo that restore (as root). The state being undone is kept, dated.
S=/opt/sftpgo-docker; U=$S.undone-$(date +%F-%H%M%S)
if [ -d $S.previous ] && [ ! -e $U ]; then docker compose -f $S/compose.yaml down &&
mv $S $U && mv $S.previous $S && docker compose -f $S/compose.yaml up -d && echo "undone; kept in $U"
else echo "STOP - there is no $S.previous to go back to"; fi
Happy with a restore? Delete /opt/sftpgo-docker.previous; otherwise the next restore stops at its first check. To put clients' files back too: [ ! -e /srv/sftpgo.previous ] && docker compose -f /opt/sftpgo-docker/compose.yaml stop && mv /srv/sftpgo{,.previous}, then rsync -a them back, give them to the container user with chown -hR 1000:1000 /srv/sftpgo, and then start it.
Moving to a brand-new server? As root there: Docker Engine 28 or newer; the firewall commands from the install block, with 80,443 added if you had a certificate; the archive in /root/sftpgo-copies; /srv/sftpgo back via rsync -a and chown -hR 1000:1000 /srv/sftpgo, before anything else so Docker cannot create it empty; then the restore lines; on a machine with no earlier install they go straight to unpacking.
Updating: take a copy, edit the release number on the image: line, then from /opt/sftpgo-docker run docker compose pull and, when that succeeds, docker compose up -d. SFTPGo upgrades its database on start by default (update_mode 0), so going back means restoring the copy, which holds the old tag too.
Questions people ask
- Can my clients upload without installing anything?
- Yes. The WebClient at
/web/clientworks in a browser: they can upload, download several files as one zip, and make share links that expire or need a password. Here it is served over HTTPS on port 443. - Why port 2022 and not 22?
- Port 22 already belongs to the server's own SSH, which you need to manage the machine. 2022 is SFTPGo's default; in WinSCP or FileZilla you type it in the port box, and on the command line it is
sftp -P 2022. Keeping them apart means a mistake in SFTPGo never locks you out of the server. - What happens to an upload if I restart SFTPGo?
- Unless you set
SFTPGO_GRACE_TIME, it stops immediately and the transfer breaks. With a grace time in seconds, it refuses new connections and lets current ones finish. Docker only waits 10 seconds by default, so raise the service'sstop_grace_periodabove that number too.