Setup vsftpd Docker Container for Raspberry Pi
6 min read
Dockerized vsftpd on Raspberry Pi (arm64)
A complete, reusable walkthrough for deploying vsftpd in Docker with:
- Multiple storage mount points
- Read-only and read-write users
- Proper Linux permission enforcement
- Passive mode fixes behind Docker NAT
- UID/GID alignment between host and container
- ACL cleanup and troubleshooting
This guide is intentionally generic so it can be reused on any host.
Environment
- OS: Debian / Raspberry Pi OS
- Architecture:
arm64 / aarch64 - Docker + Docker Compose
- Image:
forumi0721/alpine-vsftpd:aarch64
1. Prepare Host Storage
Example generic layout (mounted at fixed paths via fstab):
/srv/ftp/storage1/public
/srv/ftp/storage1/private
/srv/ftp/storage2/media
/srv/ftp/storage2/restricted
Mount USB drives at fixed paths (fstab)
Identify the drive UUID and filesystem type:
lsblk -f
blkid
Create the mount points and FTP subfolders:
sudo mkdir -p /srv/ftp/storage1/{public,private} /srv/ftp/storage2/{media,restricted}
Add entries to /etc/fstab using UUIDs so mount points never change (replace placeholders):
# ext4 (recommended, full POSIX perms)
UUID=<UUID_STORAGE1> /srv/ftp/storage1 ext4 defaults,noatime,nofail,x-systemd.device-timeout=10s 0 2
# exFAT or NTFS3 (permissions are mapped via mount options)
UUID=<UUID_STORAGE2> /srv/ftp/storage2 exfat defaults,uid=1000,gid=1000,umask=022,nofail,x-systemd.device-timeout=10s 0 0
# For NTFS, use: ntfs3
Example fstab (ext4 on both drives)
Example from one host with two ext4 USB disks. Replace UUIDs and mount points with your own:
# ADATA1TB -> /srv/ftp/storage1
UUID=3afa249d-130f-4b59-99b2-a353d0f69311 /srv/ftp/storage1 ext4 defaults,noatime,nofail,x-systemd.device-timeout=10s 0 2
# WD1TB -> /srv/ftp/storage2
UUID=10fb607b-1e19-4b9b-a5c4-11046f016366 /srv/ftp/storage2 ext4 defaults,noatime,nofail,x-systemd.device-timeout=10s 0 2
Checklist for each fstab line:
- UUID is from
lsblk -forblkid - Mount point exists and matches your docker-compose volume paths
- Filesystem type matches the disk
- Options include
nofailand a shortx-systemd.device-timeout
Notes:
- Use the filesystem type reported by
lsblk -f(ext4, exfat, ntfs3). - exFAT/NTFS do not honor per-file ownership and permissions; access is controlled by mount options.
- For on-demand mounts while keeping fixed paths, add
x-systemd.automount,x-systemd.idle-timeout=30to the options list.
Mount and verify:
sudo mount -a
findmnt -no SOURCE,FSTYPE,OPTIONS /srv/ftp/storage1 /srv/ftp/storage2
Raspberry Pi OS Desktop automount
Disable the file manager automount so drives are not mounted under /media/$USER and /srv/ftp/... stays populated:
- Open File Manager.
- Go to Edit -> Preferences -> Volume Management.
- Uncheck “Mount removable media automatically” and “Show available options for removable media on insertion”.
If you want to disable desktop automounts entirely (affects all removable media), stop udisks2:
sudo systemctl disable --now udisks2.service
If a USB drive is underpowered or slow to spin up, it may fail to mount during boot. Use a powered USB hub or add a longer
x-systemd.device-timeoutvalue if you see intermittent mount failures.
Filesystem choice (ext4 vs exFAT/NTFS3)
| FS | Pros | Cons | Best for | |—-|——|——|———-| | ext4 | True POSIX perms, ACLs, symlinks | Not native on Windows | Mixed RO/RW folders and strict permissions | | exFAT | Cross-platform | Permissions are mapped via mount options | Simple shared storage with uniform access | | NTFS (ntfs3) | Cross-platform, journaling | Permissions are mapped via mount options | Windows compatibility with large files |
If you need per-directory RO/RW on the same disk, use ext4. With exFAT/NTFS, keep RO and RW data on separate disks or accept a single permission model for the whole volume.
Permission goals
public,media: readable by all usersprivate,restricted: admin-only
sudo chmod 755 /srv/ftp/storage1/public /srv/ftp/storage2/media
sudo chmod 750 /srv/ftp/storage1/private /srv/ftp/storage2/restricted
If you used exFAT/NTFS, chmod/chown will not apply; use mount options and avoid mixing RO/RW folders on the same volume.
Docker and vsftpd do not bypass Linux permissions.
2. Create Project Directory
mkdir -p ~/vsftpd
cd ~/vsftpd
3. docker-compose.yml
services:
vsftpd:
build: .
container_name: vsftpd
restart: unless-stopped
ports:
- "21:21/tcp"
- "60000-60099:60000-60099/tcp"
environment:
FTP_USER_RW: ftpadmin
FTP_PASS_RW: adminpassword
FTP_USER_RO: ftpuser
FTP_PASS_RO: readonlypassword
volumes:
- /srv/ftp/storage1/public:/data/public:ro
- /srv/ftp/storage1/private:/data/private:rw
- /srv/ftp/storage2/media:/data/media:ro
- /srv/ftp/storage2/restricted:/data/restricted:rw
4. vsftpd.conf
listen=YES
listen_ipv6=NO
anonymous_enable=NO
local_enable=YES
write_enable=YES
local_umask=022
chroot_local_user=YES
allow_writeable_chroot=YES
local_root=/data
pasv_enable=YES
pasv_min_port=60000
pasv_max_port=60099
pasv_address=<HOST_IP>
pasv_addr_resolve=NO
5. entrypoint.sh
#!/bin/sh
set -e
# Create users with fixed IDs for permission stability
adduser -D -u 1000 ftpadmin || true
adduser -D -u 1001 ftpuser || true
echo "ftpadmin:${FTP_PASS_RW}" | chpasswd
echo "ftpuser:${FTP_PASS_RO}" | chpasswd
# Extra guard: restrict sensitive directories
chmod 750 /data/private /data/restricted 2>/dev/null || true
exec /usr/sbin/vsftpd /etc/vsftpd/vsftpd.conf
6. Dockerfile
FROM forumi0721/alpine-vsftpd:aarch64
COPY vsftpd.conf /etc/vsftpd/vsftpd.conf
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
7. Build and Run
docker compose up -d --build
8. Passive Mode NAT Fix (Critical)
If clients hang on directory listing and logs show:
227 Entering Passive Mode (172.18.x.x)
Set:
pasv_address=<HOST_IP>
Rebuild the container after changes.
9. UID/GID Alignment (Most Common Failure)
Check container user IDs:
docker exec -it vsftpd id ftpadmin
Example:
uid=1000(ftpadmin) gid=1000(ftpadmin)
Align ownership on host:
chown -R 1000:1000 /srv/ftp/storage1/public /srv/ftp/storage1/private /srv/ftp/storage2/media /srv/ftp/storage2/restricted
Apply permissions:
chmod 755 /srv/ftp/storage1/public /srv/ftp/storage2/media
chmod 750 /srv/ftp/storage1/private /srv/ftp/storage2/restricted
10. ACL Cleanup (Hidden Issue)
If permissions show a +:
ls -ld private
Install ACL tools:
apt install acl
Remove ACLs recursively:
setfacl -Rb private
setfacl -Rb restricted
Verify ACLs are gone:
getfacl -p private
11. Final Verification
docker exec -it vsftpd sh -lc 'su -s /bin/sh -c "cd /data/private && echo ftpadmin OK" ftpadmin && su -s /bin/sh -c "cd /data/private" ftpuser || echo "ftpuser blocked"'
Expected:
ftpadmin OK
ftpuser blocked
Final Access Matrix
| User | public | media | private | restricted |
|---|---|---|---|---|
| ftpadmin | RW | RW | RW | RW |
| ftpuser | RO | RO | ❌ | ❌ |
Lessons Learned
- Docker does not override Linux permissions
- UID/GID alignment is mandatory for bind mounts
- ACLs silently override
chmod - vsftpd passive mode must advertise the host IP
- Mount storage at fixed paths via fstab; avoid desktop automounters
This guide is intentionally generic and reusable for future deployments.