Set Up an FTP Server on a VPS with vsftpd (TLS + Chroot)
If you moved from cPanel shared hosting to a bare VPS, you probably noticed FTP just isn't there anymore. No File Manager, no "FTP Accounts" tab — just a blank server and whatever you install yourself. If a legacy tool, a client's desktop app, or an old deployment script still needs FTP (not SFTP), you'll have to stand up the server yourself. Here's how to do it with vsftpd without leaving an open door on your VPS.
Why this is different from cPanel FTP
On shared hosting, cPanel creates the FTP account, jails it to the right directory, and opens the ports for you. On a VPS, none of that exists until you build it. That means three things you're now responsible for: who the FTP user actually is on the Linux system, how tightly they're locked into their own directory, and which ports your firewall (and your cloud provider's security group, if you're on AWS/DigitalOcean/etc.) needs to allow.
Get any one of those wrong and you either end up with a server that refuses to start, or worse, an FTP user who can wander outside their folder.
Before you start
- Root or sudo access to the VPS (Ubuntu/Debian or AlmaLinux/RHEL steps are both below)
- A domain or static IP pointing at the server, if clients will connect by hostname
- Access to your firewall rules — both the OS firewall (ufw/firewalld) and any cloud security group
- A decision up front: do you actually need FTP, or would SFTP (which rides on SSH, port 22, and needs none of this) do the job? If you control the client, use SFTP. If a legacy app forces plain FTP, keep reading.
Step 1: Install vsftpd
vsftpd ("very secure FTP daemon") is the standard choice on Linux — small, fast, and it doesn't run as root once it's serving connections.
# Ubuntu / Debian
sudo apt update
sudo apt install vsftpd -y
# AlmaLinux / Rocky / CentOS
sudo dnf install vsftpd -y
sudo systemctl enable --now vsftpd
Back up the default config before touching anything — you'll want it if you need to compare later:
sudo cp /etc/vsftpd.conf /etc/vsftpd.conf.bak
Step 2: Create a dedicated FTP user
Don't hand out the root account or an existing sudo user for FTP. Create one scoped to the folder they should live in.
sudo adduser ftpuser
sudo mkdir -p /home/ftpuser/uploads
sudo chown nobody:nogroup /home/ftpuser
sudo chmod a-w /home/ftpuser
sudo chown ftpuser:ftpuser /home/ftpuser/uploads
That ownership split matters more than it looks. vsftpd's chroot feature refuses to jail a user into a directory that user can write to — it's a security check baked into the daemon, not a bug. The home directory (/home/ftpuser) has to be owned by someone else and non-writable; the actual working folder (uploads) is where the FTP user gets write access.
Step 3: Lock the config down
Open /etc/vsftpd.conf and set these explicitly — most distros ship with some of these commented out or on defaults you don't want:
| Directive | Value | Why |
|---|---|---|
| anonymous_enable | NO | No anonymous uploads/downloads, ever |
| local_enable | YES | Allow local system users to log in |
| write_enable | YES | Allow uploads (read-only server? set NO) |
| chroot_local_user | YES | Jail every user to their own home directory |
| allow_writeable_chroot | NO | Enforce the non-writable-home rule from Step 2 |
| user_sub_token | $USER | Used with local_root below for per-user folders |
| local_root | /home/$USER/uploads | Land users straight in their upload folder |
| pasv_enable | YES | Needed for almost every modern FTP client |
Restart to apply, then check the logs before moving on:
sudo systemctl restart vsftpd
sudo tail -f /var/log/vsftpd.log
Step 4: Add TLS — plain FTP sends passwords in cleartext
By default, FTP logins go over the wire unencrypted. Anyone on the same network path can read the username and password in plain text. Fix it with a self-signed cert (fine for internal/legacy tools) or a real one if clients will complain about warnings.
sudo mkdir -p /etc/vsftpd
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /etc/vsftpd/vsftpd.pem -out /etc/vsftpd/vsftpd.pem
Then add to /etc/vsftpd.conf:
rsa_cert_file=/etc/vsftpd/vsftpd.pem
rsa_private_key_file=/etc/vsftpd/vsftpd.pem
ssl_enable=YES
force_local_data_ssl=YES
force_local_logins_ssl=YES
ssl_tlsv1_2=YES
ssl_sslv2=NO
ssl_sslv3=NO
This turns your server into FTPS (FTP over TLS) — same port 21, but the client has to explicitly request "explicit FTP over TLS" in its connection settings, or the login will just hang.
Step 5: Fix passive mode and the firewall (the part everyone gets stuck on)
This is where 90% of "it connects but then times out on directory listing" reports come from. FTP in passive mode opens a second, random high port for data transfer — and if your firewall only allows port 21, that second connection dies silently.
Pin the passive port range in /etc/vsftpd.conf instead of leaving it random:
pasv_enable=YES
pasv_min_port=40000
pasv_max_port=40100
pasv_address=YOUR_SERVER_PUBLIC_IP
pasv_address matters if your VPS sits behind NAT or a cloud load balancer — without it, vsftpd hands the client its private internal IP for the data connection, which the client obviously can't reach.
Now open exactly those ports, plus 21, in both the OS firewall and your cloud provider's security group (they're separate — missing either one breaks it the same way):
# ufw (Ubuntu/Debian)
sudo ufw allow 21/tcp
sudo ufw allow 40000:40100/tcp
# firewalld (AlmaLinux/RHEL)
sudo firewall-cmd --permanent --add-port=21/tcp
sudo firewall-cmd --permanent --add-port=40000-40100/tcp
sudo firewall-cmd --reload
Restart vsftpd once more, then test from a client with FTPS (explicit TLS) enabled and passive mode on.
Common errors and what actually causes them
| Error | Real cause |
|---|---|
| 500 OOPS: vsftpd: refusing to run with writable root inside chroot | The user's home directory is writable by that same user. Fix ownership per Step 2 — vsftpd won't start the session otherwise. |
| Connects, login OK, then hangs on file listing | Passive port range not open in the firewall or cloud security group. See Step 5. |
| 530 Login incorrect | Either wrong credentials, or the user's shell is set to /usr/sbin/nologin and isn't in /etc/shells — some vsftpd builds check this even for FTP-only accounts. |
| Client shows "AUTH TLS failed" or a certificate warning | Client isn't set to explicit FTPS, or it's rejecting the self-signed cert — either switch to explicit TLS in the client or install a real certificate. |
Prevention: keep this server from becoming your weak point
- Add the FTP port to your
fail2banjails — brute-force login attempts against FTP are constant and automated. - Never reuse an existing sudo/admin Linux account for FTP; a compromised FTP password shouldn't hand over shell access.
- Rotate the passive port range and firewall rules if you ever open the server up more broadly (e.g. adding a second FTP user for a different client).
- Check
/var/log/vsftpd.logperiodically, or ship it to your monitoring tool — repeated failed logins from one IP is your early warning. - If you can migrate the workflow to SFTP later, do it — it uses your existing SSH hardening (keys, fail2ban, no separate cert management) instead of a whole parallel stack.
Frequently asked questions
Do I need vsftpd if my VPS already has SSH access?
Not necessarily. SSH access already gives you SFTP, which is encrypted by default and needs no extra ports or certificates. Only set up vsftpd if a specific tool or client can't be switched to SFTP.
Why does vsftpd refuse to start with a "writable root inside chroot" error?
vsftpd checks that a chrooted user's home directory isn't writable by that same user, since a writable chroot root can be used to escape the jail. Make the home directory owned by root or nobody and non-writable, then give the user write access to a subfolder inside it instead.
Can I run vsftpd and SFTP on the same VPS at the same time?
Yes, they don't conflict — vsftpd listens on port 21 (or 990 for implicit FTPS) and SFTP rides on port 22 through SSH. Just make sure both sets of firewall rules are correct.
My FTP client connects but the file listing never loads. What's wrong?
Almost always a passive-mode port problem. Pin pasv_min_port and pasv_max_port in vsftpd.conf, then open that exact range in both your OS firewall and your cloud provider's security group.
Is a self-signed certificate good enough for FTPS?
It's fine for internal tools or a client you control, since the connection is still encrypted. For third-party clients who'll see certificate warnings, use a free Let's Encrypt certificate instead — the setup in vsftpd.conf is identical, just point rsa_cert_file at the Let's Encrypt fullchain and key.