A dead simple SFTP server in Docker. Nothing else. No shell, no tunneling, no forwarding. Just SFTP.
These are pre-built and hosted. Pick one — they're the same image:
pacnpal/simple-sftp-server
ghcr.io/pacnpal/simple-sftp-server
Create host directories first (recommended):
mkdir -p /home/user/.ssh_keys/simple-sftp /home/user/.ssh_keys/simple-sftp-host /home/user/sftp_data
chmod 700 /home/user/.ssh_keys /home/user/.ssh_keys/simple-sftp /home/user/.ssh_keys/simple-sftp-host
docker run -d --name simple-sftp-server \
--user "$(id -u):$(id -g)" \
--cap-add SYS_CHROOT \
-p 2222:2022 \
-e PUID="$(id -u)" \
-e PGID="$(id -g)" \
-e SSH_KEY_DIR=/keys \
-e HOST_KEY_DIR=/host_keys \
-e SFTP_CHROOT=true \
-v /home/user/.ssh_keys/simple-sftp:/keys \
-v /home/user/.ssh_keys/simple-sftp-host:/host_keys \
-v /home/user/sftp_data:/home/sftpuser/data \
pacnpal/simple-sftp-server
Recommended mode is jailed (SFTP_CHROOT=true).
Chroot mode confines authenticated users to /home/sftpuser, which reduces accidental exposure of unrelated mounted paths.
Chroot mode requires SYS_CHROOT capability at runtime.
This project is provided as-is, without warranty or support. Operators are responsible for secure deployment.
This image intentionally stores generated client keys on the host for convenience. You are responsible for securing /home/user/.ssh_keys/simple-sftp.
Use non-overlapping host directories for /keys, /host_keys, and /home/sftpuser/data.
On first start, a keypair is generated. The private key is already on your host:
cp /home/user/.ssh_keys/simple-sftp/sftpuser_key ./sftp_key && chmod 600 ./sftp_key
Windows users: The
chmodcommand doesn't exist on Windows. Instead, right-click the file > Properties > Security > make sure only your user has access.
sftp -i sftp_key -P 2222 sftpuser@localhost
You should see:
Connected to sftpuser@localhost.
sftp>
You're in. Type ls to look around, put myfile.txt to upload, get myfile.txt to download, or quit to disconnect.
# Stop the server
docker stop simple-sftp-server
# Start it back up (your files and keys are still there)
docker start simple-sftp-server
# Remove it completely
docker rm -f simple-sftp-server
Your files and keys are stored on the host filesystem, so they persist across stop, start, and even docker rm + recreate. They only go away if you explicitly delete the directories:
# WARNING: This deletes all generated keys, host keys, and uploaded files
rm -rf /home/user/.ssh_keys/simple-sftp /home/user/.ssh_keys/simple-sftp-host /home/user/sftp_data
If you prefer Docker Compose, create a file called docker-compose.yml:
services:
sftp:
image: pacnpal/simple-sftp-server:latest
user: "${PUID:-1000}:${PGID:-1000}"
cap_add:
- SYS_CHROOT
ports:
- "${SFTP_HOST_PORT:-2222}:2022"
environment:
- PUID=1000
- PGID=1000
- SSH_KEY_DIR=/keys
- HOST_KEY_DIR=/host_keys
- SFTP_CHROOT=true
volumes:
- /home/user/.ssh_keys/simple-sftp:/keys
- /home/user/.ssh_keys/simple-sftp-host:/host_keys
- /home/user/sftp_data:/home/sftpuser/data
Then run:
docker compose up -d
Everything else is the same — get the key and connect as described above.
If you already have SSH keys and an authorized_keys file, you can skip the auto-generated key.
Put authorized_keys inside a host directory, then mount that directory as SSH_KEY_DIR.
docker run -d --name simple-sftp-server \
--user "$(id -u):$(id -g)" \
--cap-add SYS_CHROOT \
-p 2222:2022 \
-e PUID="$(id -u)" \
-e PGID="$(id -g)" \
-e SSH_KEY_DIR=/keys \
-e HOST_KEY_DIR=/host_keys \
-e SFTP_CHROOT=true \
-v /path/to/your/keydir:/keys \
-v /home/user/.ssh_keys/simple-sftp-host:/host_keys \
-v /home/user/sftp_data:/home/sftpuser/data \
pacnpal/simple-sftp-server
Replace /path/to/your/keydir with the directory containing authorized_keys.
authorized_keys is copied at startup to /tmp/simple-sftp-runtime/authorized_keys with strict runtime permissions.HOST_KEY_DIR (/host_keys by default). Missing host key types are auto-generated on startup.authorized_keys can be mounted read-only; write access is only required for first-run key generation.sshd template at build time (see sshd_config.template in this repo).sshd -D -e -f /tmp/simple-sftp-runtime/sshd_config.runtimeAuthorizedKeysFile is pinned to /tmp/simple-sftp-runtime/authorized_keys to avoid host mount permission edge cases.2022; change only the host publish port (-p <host_port>:2022).SFTP_CHROOT=true (default/recommended): enables ChrootDirectory /home/sftpuser.SFTP_CHROOT=false: disables filesystem jail.CAP_SYS_CHROOT (--cap-add SYS_CHROOT / Compose cap_add: [SYS_CHROOT])./home/sftpuser.If this fails even with the right private key:
sftp -o IdentitiesOnly=yes -o IdentityAgent=none -i sftp_key -P 2222 sftpuser@localhost
Verify the runtime sshd view:
docker exec <container_name> sh -lc 'sshd -T -C user=sftpuser,host=localhost,addr=127.0.0.1 | grep authorizedkeysfile'
docker exec <container_name> sh -lc 'ssh-keygen -lf /tmp/simple-sftp-runtime/authorized_keys'
ssh-keygen -y -f sftp_key | ssh-keygen -lf -
The authorizedkeysfile path should be /tmp/simple-sftp-runtime/authorized_keys, and the two fingerprints should match.
server lacks privileges to chroot to ChrootDirectoryThis means SFTP_CHROOT=true but the container does not have CAP_SYS_CHROOT.
Fix:
docker run: add --cap-add SYS_CHROOTcap_add:
- SYS_CHROOT
If your runtime policy does not allow this capability, set SFTP_CHROOT=false and accept the no-chroot risk model.
If clients warn about host key changes, your host-key mount is not persisting stable key files.
docker inspect <container_name> --format '{{range .Mounts}}{{.Destination}} <- {{.Source}}{{println}}{{end}}'
ls -l /home/user/.ssh_keys/simple-sftp-host/ssh_host_*
Keep /host_keys mounted to a stable host directory and avoid overlapping it under an SFTP-exposed parent mount.
If logs include errors like:
cp: can't open '/host_keys/ssh_host_rsa_key': Permission denied
the mounted key directories are not readable/writable by the container.
HOST_KEY_DIR and SSH_KEY_DIR mounts are readable/writable during startup.PUID/PGID.You can customize behavior with these:
| Variable | Default | What it does |
|---|---|---|
SFTP_PATHS | /data | Comma-separated directories to create (accessible over SFTP) |
SSH_KEY_DIR | /keys | Where user keys are stored inside the container |
HOST_KEY_DIR | /host_keys | Where SSH host keys are persisted inside the container |
PUID | SFTP_UID (1000 by default) | Runtime UID to run the container as |
PGID | SFTP_GID (1000 by default) | Runtime GID to run the container as |
SFTP_CHROOT | true | true enables filesystem jail (ChrootDirectory) |
SSH_KEY_DIR and HOST_KEY_DIR should be absolute paths. For backward compatibility, relative values like keys are interpreted as /keys.
To match host filesystem ownership, pass PUID and PGID at container startup:
docker run -d --name simple-sftp-server \
--user "$(id -u):$(id -g)" \
--cap-add SYS_CHROOT \
-p 2222:2022 \
-e PUID="$(id -u)" \
-e PGID="$(id -g)" \
-e SSH_KEY_DIR=/keys \
-e HOST_KEY_DIR=/host_keys \
-e SFTP_CHROOT=true \
-v /home/user/.ssh_keys/simple-sftp:/keys \
-v /home/user/.ssh_keys/simple-sftp-host:/host_keys \
-v /home/user/sftp_data:/home/sftpuser/data \
pacnpal/simple-sftp-server
PUID/PGID must match the container runtime user (user: / --user).
Example — serve multiple directories:
docker run -d --name simple-sftp-server \
--user "$(id -u):$(id -g)" \
--cap-add SYS_CHROOT \
-p 2222:2022 \
-e PUID="$(id -u)" \
-e PGID="$(id -g)" \
-e SSH_KEY_DIR=/keys \
-e HOST_KEY_DIR=/host_keys \
-e SFTP_CHROOT=true \
-e SFTP_PATHS=/data,/uploads,/backups \
-v /home/user/.ssh_keys/simple-sftp:/keys \
-v /home/user/.ssh_keys/simple-sftp-host:/host_keys \
-v /home/user/sftp_data:/home/sftpuser/data \
-v /home/user/sftp_uploads:/home/sftpuser/uploads \
-v /home/user/sftp_backups:/home/sftpuser/backups \
pacnpal/simple-sftp-server
Use this only when you intentionally accept a wider filesystem access model for authenticated users.
docker run -d --name simple-sftp-server \
--user "$(id -u):$(id -g)" \
-p 2222:2022 \
-e PUID="$(id -u)" \
-e PGID="$(id -g)" \
-e SSH_KEY_DIR=/keys \
-e HOST_KEY_DIR=/host_keys \
-e SFTP_CHROOT=false \
-v /home/user/.ssh_keys/simple-sftp:/keys \
-v /home/user/.ssh_keys/simple-sftp-host:/host_keys \
-v /home/user/sftp_data:/home/sftpuser/data \
pacnpal/simple-sftp-server
Compose equivalent:
services:
sftp:
image: pacnpal/simple-sftp-server:latest
user: "${PUID:-1000}:${PGID:-1000}"
environment:
- SFTP_CHROOT=false
If you run with SFTP_CHROOT=false, this image does not isolate the SFTP user to /home/sftpuser.
SFTP_CHROOT=true and add SYS_CHROOT.Only needed if you want to modify the server yourself:
git clone https://github.com/pacnpal/simple-sftp-server.git
cd simple-sftp-server
docker build -t simple-sftp-server .
Then use simple-sftp-server instead of pacnpal/simple-sftp-server in the commands above.
user: "${PUID}:${PGID}" / --user "${PUID}:${PGID}"PUID/PGID must match container runtime uid/gidSFTP_CHROOT=true)cap_add: [SYS_CHROOT] / --cap-add SYS_CHROOTMIT — see LICENSE.
Content type
Image
Digest
sha256:0c6a7e56c…
Size
4.1 MB
Last updated
7 months ago
docker pull pacnpal/simple-sftp-server