Setup vsftpd Docker Container for Raspberry Pi
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 (already mounted at boot):
/srv/ftp/storage1/public /srv/ftp/storage1/private /srv/ftp/storage2/media /srv/ftp/storage2/restricted Permission goals
public,media: readable by all usersprivate,restricted: admin-only
chmod 755 public media chmod 750 private restricted 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 boot, not automount
This guide is intentionally generic and reusable for future deployments.
Don't miss what's next. Subscribe to Enkiel Hub Newsletter: