Running FTP servers in Docker is a surprisingly tricky business, mostly because FTP itself is a protocol designed before firewalls were common and uses a dynamic range of ports for data transfers.
Let’s see vsftpd in action. We’ll set up a basic vsftpd server that allows anonymous access and logs to /var/log/vsftpd.log.
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y vsftpd && rm -rf /var/lib/apt/lists/*
# Anonymous read-only access
RUN sed -i 's/#anon_root=/anon_root=/' /etc/vsftpd.conf
RUN sed -i 's/#anon_enable=YES/anon_enable=YES/' /etc/vsftpd.conf
RUN sed -i 's/#write_enable=YES/write_enable=NO/' /etc/vsftpd.conf
RUN sed -i 's/#local_enable=YES/local_enable=NO/' /etc/vsftpd.conf
RUN sed -i 's/#chroot_local_user=YES/chroot_local_user=NO/' /etc/vsftpd.conf
# Passive mode configuration
RUN echo "pasv_enable=YES" >> /etc/vsftpd.conf
RUN echo "pasv_min_port=40000" >> /etc/vsftpd.conf
RUN echo "pasv_max_port=50000" >> /etc/vsftpd.conf
RUN echo "pasv_address=192.168.1.100" >> /etc/vsftpd.conf # Replace with your host's actual IP or public IP
EXPOSE 21
EXPOSE 40000-50000
CMD ["/usr/sbin/vsftpd", "/etc/vsftpd.conf"]
Now, build and run this:
docker build -t my-vsftpd .
docker run -d -p 21:21 -p 40000-50000:40000-50000 --name ftp-server my-vsftpd
When you connect with an FTP client (like lftp or FileZilla), you’ll see the server respond on port 21 for commands. However, for file transfers, it will initiate connections back to your client on ports within the 40000-50000 range. This is the "passive mode" that helps with firewalls, but it’s still a bit of a dance.
The core problem vsftpd (and ProFTPD) solves is providing a standard way to transfer files over a network. Internally, it manages connections. The command channel (port 21) is for instructions, and the data channel is for the actual file content. In active mode, the server initiates the data connection back to the client. In passive mode, the client initiates the data connection to the server on a port range specified by the server. Docker’s network isolation, combined with the dynamic nature of the data channel, is what makes this complex.
The pasv_address configuration is critical if your Docker host is behind NAT or has multiple network interfaces. Without it, the server might tell clients to connect back to an internal Docker IP address that’s unreachable from the outside. You must set this to the IP address that clients will use to reach your Docker host.
The most surprising thing about running FTP in containers is how much manual configuration is required to make it behave predictably, especially concerning passive mode ports. Most modern services are designed with stateless, ephemeral connections in mind, or use protocols that are more firewall-friendly like SFTP (which runs over SSH).
The pasv_min_port and pasv_max_port settings in vsftpd.conf are where you define the range of ports the server can use for data transfers in passive mode. These ports must then be exposed both in your Dockerfile’s EXPOSE directive and, crucially, in your docker run command’s port mapping. If you map -p 40000-50000:40000-50000, Docker translates connections to these ports on your host to the corresponding ports inside the container.
If you’re using vsftpd and see "500 OOPS: vsftpd: refusing to run with writable root shell" during startup, it means vsftpd is configured to disallow users from having a writable root directory, which is a security feature. You’d need to adjust chroot_local_user and potentially allow_writeable_chroot in vsftpd.conf if you intend to allow local user logins with write access.
The next hurdle you’ll likely face is managing users and permissions within the Docker container, especially if you want to avoid anonymous access and maintain persistent storage for uploaded files.