Containerized FTP server with REST API and web dashboard for user management.
- Overview
- Key Features
- Quick Start
- Configuration
- API Reference
- Integration Examples
- Security
- Contributing
- Support
- License
vsftpd-manager is a production-ready, containerized FTP server built on vsftpd (Alpine Linux) with a FastAPI-powered REST API and web dashboard for managing FTP users. Users are persisted in a JSON file and survive container recreations. Each user is chrooted to their own home directory for isolation.
- vsftpd FTP Server -- Lightweight, secure FTP over Alpine Linux
- REST API -- Full CRUD for user management via FastAPI
- Web Dashboard -- Built-in UI with dark/light mode for managing users
- Persistent Users -- JSON-based persistence across container restarts
- Per-User Chroot -- Each user is confined to their home directory
- Token Authentication -- API protected by constant-time token comparison
- Rate Limiting -- In-memory per-IP rate limiting (30 req/min)
- Security Headers -- X-Frame-Options, CSP, XSS protection, and more
- Traefik Ready -- Labels included for HTTPS via Traefik + Cloudflare
- Multi-arch -- Docker images built for linux/amd64 and linux/arm64
docker pull ghcr.io/onflux-tech/vsftpd-manager:latestservices:
ftp-server:
image: ghcr.io/onflux-tech/vsftpd-manager:latest
container_name: ftp-server
restart: unless-stopped
environment:
USERS: "admin|StrongPassword123|/ftp/admin"
ADMIN_TOKEN: change-this-to-a-strong-token
ADDRESS: ftp.example.com
MIN_PORT: 50000
MAX_PORT: 50100
MANAGER_PORT: 8080
TZ: America/Sao_Paulo
volumes:
- ./ftp-data:/ftp
- ./data:/data
ports:
- "21:21"
- "50000-50100:50000-50100"
- "8080:8080"docker compose up -dAccess the web dashboard at http://localhost:8080.
git clone https://github.com/onflux-tech/vsftpd-manager.git
cd vsftpd-manager
docker compose up -d --build| Variable | Default | Description |
|---|---|---|
USERS |
(empty) | Bootstrap users in name|password|home format, space-separated |
ADMIN_TOKEN |
changeme |
Authentication token for API and dashboard |
ADDRESS |
(empty) | Hostname for FTP passive address |
MIN_PORT |
50000 |
Minimum passive port range |
MAX_PORT |
50100 |
Maximum passive port range |
MANAGER_PORT |
8080 |
API/dashboard listen port |
TZ |
America/Sao_Paulo |
Container timezone |
| Port | Service |
|---|---|
| 21 | FTP (vsftpd) |
| 50000-50100 | FTP passive range |
| 8080 | REST API + Web dashboard |
.
├── Dockerfile # Multi-stage build (pidproxy + vsftpd + Python venv)
├── docker-compose.yml # Orchestration with volumes and optional Traefik labels
├── manager_api.py # REST API + HTML dashboard (FastAPI/Uvicorn)
├── start_vsftpd.sh # Entrypoint: creates env users + restores JSON + starts vsftpd
├── vsftpd.conf # vsftpd configuration
└── data/
└── users.json # Persisted user database (created automatically)
All API routes (except /api/health) require authentication via one of:
X-API-Key: <ADMIN_TOKEN>
or
Authorization: Bearer <ADMIN_TOKEN>
curl http://localhost:8080/api/health{"status": "ok"}curl -H "X-API-Key: YOUR_TOKEN" http://localhost:8080/api/users[
{
"username": "demo",
"uid": 1000,
"gid": 1000,
"home": "/ftp/demo",
"managed": true
}
]The managed field indicates whether the user was created via the API (true) or via the USERS environment variable (false).
curl -X POST http://localhost:8080/api/users \
-H "X-API-Key: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"username": "client1", "password": "StrongPass123!", "home": "/ftp/client1"}'The home field is optional. If omitted, defaults to /ftp/<username>.
{"username": "client1", "home": "/ftp/client1"}curl -X PUT http://localhost:8080/api/users/client1/password \
-H "X-API-Key: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"password": "NewPassword456!"}'{"username": "client1", "updated": true}curl -X DELETE -H "X-API-Key: YOUR_TOKEN" http://localhost:8080/api/users/client1Response: 204 No Content
import requests
BASE = "http://localhost:8080"
HEADERS = {"X-API-Key": "your-token-here"}
# List users
users = requests.get(f"{BASE}/api/users", headers=HEADERS).json()
print(f"Total: {len(users)} users")
# Create user
requests.post(f"{BASE}/api/users", headers=HEADERS, json={
"username": "new_client",
"password": "Pass123!",
"home": "/ftp/new_client"
})
# Change password
requests.put(f"{BASE}/api/users/new_client/password", headers=HEADERS, json={
"password": "AnotherPass456!"
})
# Delete user
requests.delete(f"{BASE}/api/users/new_client", headers=HEADERS)const BASE = "http://localhost:8080";
const headers = {
"X-API-Key": "your-token-here",
"Content-Type": "application/json",
};
// List users
const users = await fetch(`${BASE}/api/users`, { headers }).then(r => r.json());
console.log(`Total: ${users.length} users`);
// Create user
await fetch(`${BASE}/api/users`, {
method: "POST",
headers,
body: JSON.stringify({
username: "new_client",
password: "Pass123!",
home: "/ftp/new_client",
}),
});
// Change password
await fetch(`${BASE}/api/users/new_client/password`, {
method: "PUT",
headers,
body: JSON.stringify({ password: "AnotherPass456!" }),
});
// Delete user
await fetch(`${BASE}/api/users/new_client`, {
method: "DELETE",
headers,
});- Mandatory token for all routes (except
/api/health) - Constant-time token comparison via
hmac.compare_digest(timing-attack resistant) - Per-IP rate limiting (30 requests per minute)
- Security headers:
X-Frame-Options,X-Content-Type-Options,X-XSS-Protection,Referrer-Policy,Permissions-Policy - Chroot enabled: each FTP user is confined to their home directory
- Dashboard login screen (token stored in sessionStorage, not persisted across sessions)
- HTML output sanitization against XSS
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Issues: GitHub Issues
- Questions: Open an issue with the "question" label
This project is licensed under the MIT License -- see the LICENSE file for details.
- Visao Geral
- Principais Recursos
- Inicio Rapido
- Configuracao
- Referencia da API
- Exemplos de Integracao
- Seguranca
- Contribuindo
- Suporte
- Licenca
vsftpd-manager e um servidor FTP containerizado e pronto para producao, construido sobre vsftpd (Alpine Linux) com uma API REST e painel web alimentados por FastAPI para gerenciamento de usuarios FTP. Os usuarios sao persistidos em arquivo JSON e sobrevivem a recriacoes do container. Cada usuario e confinado (chroot) ao seu proprio diretorio.
- Servidor FTP vsftpd -- Leve e seguro sobre Alpine Linux
- API REST -- CRUD completo para gerenciamento de usuarios via FastAPI
- Painel Web -- Interface integrada com modo escuro/claro para gerenciar usuarios
- Usuarios Persistentes -- Persistencia em JSON entre reinicializacoes do container
- Chroot por Usuario -- Cada usuario fica restrito ao seu diretorio home
- Autenticacao por Token -- API protegida com comparacao de token em tempo constante
- Rate Limiting -- Limitacao de requisicoes por IP em memoria (30 req/min)
- Headers de Seguranca -- X-Frame-Options, CSP, protecao XSS e mais
- Pronto para Traefik -- Labels incluidas para HTTPS via Traefik + Cloudflare
- Multi-arch -- Imagens Docker construidas para linux/amd64 e linux/arm64
docker pull ghcr.io/onflux-tech/vsftpd-manager:latestservices:
ftp-server:
image: ghcr.io/onflux-tech/vsftpd-manager:latest
container_name: ftp-server
restart: unless-stopped
environment:
USERS: "admin|SenhaForte123|/ftp/admin"
ADMIN_TOKEN: troque-por-um-token-forte
ADDRESS: ftp.exemplo.com
MIN_PORT: 50000
MAX_PORT: 50100
MANAGER_PORT: 8080
TZ: America/Sao_Paulo
volumes:
- ./ftp-data:/ftp
- ./data:/data
ports:
- "21:21"
- "50000-50100:50000-50100"
- "8080:8080"docker compose up -dAcesse o painel web em http://localhost:8080.
git clone https://github.com/onflux-tech/vsftpd-manager.git
cd vsftpd-manager
docker compose up -d --build| Variavel | Padrao | Descricao |
|---|---|---|
USERS |
(vazio) | Usuarios iniciais no formato nome|senha|home separados por espaco |
ADMIN_TOKEN |
changeme |
Token de autenticacao da API e painel |
ADDRESS |
(vazio) | Hostname para endereco passivo do FTP |
MIN_PORT |
50000 |
Porta minima do range passivo |
MAX_PORT |
50100 |
Porta maxima do range passivo |
MANAGER_PORT |
8080 |
Porta da API/painel web |
TZ |
America/Sao_Paulo |
Timezone do container |
| Porta | Servico |
|---|---|
| 21 | FTP (vsftpd) |
| 50000-50100 | FTP range passivo |
| 8080 | API REST + Painel web |
.
├── Dockerfile # Build multi-stage (pidproxy + vsftpd + Python venv)
├── docker-compose.yml # Orquestracao com volumes e labels opcionais do Traefik
├── manager_api.py # API REST + dashboard HTML (FastAPI/Uvicorn)
├── start_vsftpd.sh # Entrypoint: cria usuarios env + restaura JSON + inicia vsftpd
├── vsftpd.conf # Configuracao do vsftpd
└── data/
└── users.json # Base de usuarios persistida (criado automaticamente)
Todas as rotas da API (exceto /api/health) exigem autenticacao via:
X-API-Key: <ADMIN_TOKEN>
ou
Authorization: Bearer <ADMIN_TOKEN>
curl http://localhost:8080/api/health{"status": "ok"}curl -H "X-API-Key: SEU_TOKEN" http://localhost:8080/api/users[
{
"username": "demo",
"uid": 1000,
"gid": 1000,
"home": "/ftp/demo",
"managed": true
}
]O campo managed indica se o usuario foi criado pela API (true) ou pela variavel de ambiente USERS (false).
curl -X POST http://localhost:8080/api/users \
-H "X-API-Key: SEU_TOKEN" \
-H "Content-Type: application/json" \
-d '{"username": "cliente1", "password": "SenhaForte123!", "home": "/ftp/cliente1"}'O campo home e opcional. Se omitido, sera /ftp/<username>.
{"username": "cliente1", "home": "/ftp/cliente1"}curl -X PUT http://localhost:8080/api/users/cliente1/password \
-H "X-API-Key: SEU_TOKEN" \
-H "Content-Type: application/json" \
-d '{"password": "NovaSenha456!"}'{"username": "cliente1", "updated": true}curl -X DELETE -H "X-API-Key: SEU_TOKEN" http://localhost:8080/api/users/cliente1Resposta: 204 No Content
import requests
BASE = "http://localhost:8080"
HEADERS = {"X-API-Key": "seu-token-aqui"}
# Listar usuarios
users = requests.get(f"{BASE}/api/users", headers=HEADERS).json()
print(f"Total: {len(users)} usuarios")
# Criar usuario
requests.post(f"{BASE}/api/users", headers=HEADERS, json={
"username": "novo_cliente",
"password": "Senha123!",
"home": "/ftp/novo_cliente"
})
# Alterar senha
requests.put(f"{BASE}/api/users/novo_cliente/password", headers=HEADERS, json={
"password": "OutraSenha456!"
})
# Excluir usuario
requests.delete(f"{BASE}/api/users/novo_cliente", headers=HEADERS)const BASE = "http://localhost:8080";
const headers = {
"X-API-Key": "seu-token-aqui",
"Content-Type": "application/json",
};
// Listar usuarios
const users = await fetch(`${BASE}/api/users`, { headers }).then(r => r.json());
console.log(`Total: ${users.length} usuarios`);
// Criar usuario
await fetch(`${BASE}/api/users`, {
method: "POST",
headers,
body: JSON.stringify({
username: "novo_cliente",
password: "Senha123!",
home: "/ftp/novo_cliente",
}),
});
// Alterar senha
await fetch(`${BASE}/api/users/novo_cliente/password`, {
method: "PUT",
headers,
body: JSON.stringify({ password: "OutraSenha456!" }),
});
// Excluir usuario
await fetch(`${BASE}/api/users/novo_cliente`, {
method: "DELETE",
headers,
});- Token de acesso obrigatorio para todas as rotas (exceto
/api/health) - Comparacao de token com
hmac.compare_digest(resistente a timing attacks) - Rate limiting por IP (30 requisicoes por minuto)
- Headers de seguranca:
X-Frame-Options,X-Content-Type-Options,X-XSS-Protection,Referrer-Policy,Permissions-Policy - Chroot habilitado: cada usuario FTP fica restrito ao seu diretorio home
- Painel web com tela de login (token em sessionStorage, nao persiste entre sessoes)
- Sanitizacao de saida HTML contra XSS
Contribuicoes sao bem-vindas! Siga os passos abaixo:
- Faca um fork do repositorio
- Crie sua branch de feature (
git checkout -b feature/funcionalidade-incrivel) - Faca commit das suas mudancas (
git commit -m 'Adiciona funcionalidade incrivel') - Faca push para a branch (
git push origin feature/funcionalidade-incrivel) - Abra um Pull Request
- Issues: GitHub Issues
- Duvidas: Abra uma issue com a label "question"
Este projeto esta licenciado sob a Licenca MIT -- veja o arquivo LICENSE para detalhes.
Made with ❤️ by the OnFlux Tech team.
Star ⭐ this repository if you find it helpful!