Enkiel Hub Newsletter

Archives
December 14, 2025

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 users
  • private, 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:
Powered by Buttondown, the easiest way to start and grow your newsletter.