A minimal, secure TFTP server with optional web server based on Alpine Linux, tftpd-hpa, and BusyBox httpd.
Also available on Docker Hub: https://hub.docker.com/r/kaczmar2/tftp-server
- TFTP-only mode: Minimal TFTP server for network boot scenarios
- TFTP + Web mode: TFTP server with HTTP access to boot files and scripts
- Runtime mode selection: Single image, choose mode with environment variable
- Multi-architecture: Supports AMD64, ARM64, and ARM v7
- Security: Runs as
nobodyuser with proper privilege dropping - Logging: Unified Docker logs for both TFTP and HTTP activity
# Clone or create the project directory
mkdir -p ~/docker/tftp-server && cd ~/docker/tftp-server
# Download docker-compose.yml
curl -O https://raw.githubusercontent.com/kaczmar2/tftp-server/main/docker-compose.yml
# Create .env file for configuration (optional)
curl -O https://raw.githubusercontent.com/kaczmar2/tftp-server/main/.env.example
cp .env.example .env
# Edit .env to set TZ, TFTP_ROOT, and WEB_ROOT if needed
# Create Docker bind mount directories
sudo mkdir -p /srv/docker/tftp /srv/docker/www
# Start TFTP + Web server (default)
docker compose up -d
# OR start TFTP-only mode
docker compose --profile tftp-only up -d
# Check status
docker compose ps
docker logs tftp-server# TFTP + Web server mode
docker run -d \
--name tftp-server \
--network host \
--restart unless-stopped \
-e TZ=America/Denver \
-e ENABLE_WEBSERVER=true \
-v /srv/docker/tftp:/srv/tftp \
-v /srv/docker/www:/srv/www \
ghcr.io/kaczmar2/tftp-server
# TFTP-only mode
docker run -d \
--name tftp-server \
--network host \
--restart unless-stopped \
-e TZ=America/Denver \
-e ENABLE_WEBSERVER=false \
-v /srv/docker/tftp:/srv/tftp \
ghcr.io/kaczmar2/tftp-serverENABLE_WEBSERVER: Set totrueto enable HTTP server,falsefor TFTP-only (default:false)TZ: Timezone for logs and timestamps (default:UTC)TFTP_ARGS: Custom TFTP daemon arguments (see Custom TFTP Options section)
- Default (
docker compose up): TFTP + Web server mode tftp-only(docker compose --profile tftp-only up): TFTP-only mode
services:
# TFTP-only server
tftp-only:
container_name: tftp-server
image: ghcr.io/kaczmar2/tftp-server
restart: unless-stopped
network_mode: host
environment:
- TZ=${TZ:-UTC}
- ENABLE_WEBSERVER=false
volumes:
- ${TFTP_ROOT:-/srv/docker/tftp}:/srv/tftp
profiles:
- tftp-only
# TFTP + BusyBox httpd web server
tftp-web:
container_name: tftp-server
image: ghcr.io/kaczmar2/tftp-server
restart: unless-stopped
network_mode: host
environment:
- TZ=${TZ:-UTC}
- ENABLE_WEBSERVER=true
volumes:
- ${TFTP_ROOT:-/srv/docker/tftp}:/srv/tftp
- ${WEB_ROOT:-/srv/docker/www}:/srv/www
profiles:
- tftp-web
- default/srv/docker/tftp/ # Host directory (mapped to container /srv/tftp)
├── bootfile.txt # File to serve via TFTP
├── firmware.bin # File to serve via TFTP
└── subdirectory/ # Subdirectories are supported
└── nested-file.txt
/srv/docker/www/ # Host directory (mapped to container /srv/www)
├── index.html # Served via HTTP at http://server/
├── boot-scripts/ # Directory listing available
│ ├── script1.sh # Served via HTTP at http://server/boot-scripts/script1.sh
│ └── script2.py
└── documentation/
└── readme.txt
Install a TFTP client to test your server:
# Install TFTP client
sudo apt install tftp-hpaTest file download:
cd /tmp
uname -a | sudo tee /srv/docker/tftp/test
tftp localhost
tftp> get test
tftp> quit
diff test /srv/docker/tftp/test
# (no output = files are identical)# Create test content
echo "<h1>TFTP Boot Server</h1>" | sudo tee /srv/docker/www/index.html
echo "#!/bin/bash\necho 'Boot script executed'" | sudo tee /srv/docker/www/boot.sh
# Test HTTP access
curl http://localhost/ # Should show HTML
curl http://localhost/boot.sh # Should show script
curl -I http://localhost/ # Check headers# Real-time logs (includes TFTP and HTTP requests)
docker logs -f tftp-server
# Check for TFTP requests (RRQ = Read Request)
docker logs tftp-server | grep RRQ
# Check for TFTP errors (NAK = Negative Acknowledgment)
docker logs tftp-server | grep NAK
# Check for HTTP requests (when web server enabled)
# BusyBox httpd logs show client IP, port, and response code
docker logs tftp-server | grep "response:"Log examples:
# TFTP requests (detailed)
<29>Jan 16 10:30:15 in.tftpd[25]: RRQ from 192.168.1.100 filename bootfile.txt
# HTTP requests (minimal format - IP, port, status only)
[::ffff:10.10.10.144]:49118: response:200
[::ffff:10.10.10.145]:46906: response:404
# Service status
Starting TFTP server with process supervisor...
Web server enabled - HTTP accessible on port 80 (PID: 16)
TFTP server started (PID: 17)
BusyBox httpd provides minimal access logging with the following format:
[<client_ip>]:<client_port>: response:<status_code>
Field Breakdown:
- Client IP: IPv6-mapped IPv4 address (e.g.,
::ffff:10.10.10.144) or IPv6 address - Client Port: Ephemeral port used by the client (e.g.,
49118) - Status Code: HTTP response code (e.g.,
200,404,403)
Common HTTP Status Codes:
200- Success (file found and served)404- Not Found (requested file doesn't exist)403- Forbidden (permission denied)304- Not Modified (cached response)500- Internal Server Error
Limitations:
- ❌ No timestamps (use container logs or correlate with TFTP timestamps)
- ❌ No request method (GET, POST, etc.)
- ❌ No requested file path or URL
- ❌ No bytes transferred or user agent
- ✅ Minimal overhead and ultra-lightweight
Workaround for file tracking: If you need to know which files are being accessed via HTTP, monitor file access times on the host:
# Check which files were recently accessed
ls -ltu /srv/docker/www/Note: For production environments requiring detailed access logs, consider using a dedicated web server like lighttpd or nginx instead of BusyBox httpd.
# Add TFTP files
cp bootfile.txt /srv/docker/tftp/
chmod 644 /srv/docker/tftp/*
# Add web files (when using web server)
cp index.html /srv/docker/www/
cp -r boot-scripts/ /srv/docker/www/
chmod 644 /srv/docker/www/* /srv/docker/www/**/*
# Check what files are available
ls -la /srv/docker/tftp/ # TFTP files
ls -la /srv/docker/www/ # Web files (if enabled)This container requires network_mode: host because:
- TFTP uses dynamic ports - Data transfers use random ephemeral ports
- Port mapping doesn't work - Docker can't map unknown future ports
- Host networking is standard - Most TFTP Docker images use this approach
TFTP (always required):
- UDP port 69 must be accessible
Web server (when ENABLE_WEBSERVER=true):
- TCP port 80 must be accessible
The container supports customizing TFTP daemon behavior via the TFTP_ARGS environment variable. You can pass any valid in.tftpd options while keeping the current defaults as the base.
# Start container with --create flag to enable uploads
docker run \
--network host \
-e ENABLE_WEBSERVER=false \
-e TFTP_ARGS="--foreground --secure --create --verbosity 4 --user nobody" \
-v /srv/docker/tftp:/srv/tftp \
ghcr.io/kaczmar2/tftp-serverAdd to your .env file:
# Enable write access with custom settings
TFTP_ARGS=--foreground --secure --create --verbosity 4 --user nobodySee the tftpd man page for all available options.
When customizing TFTP_ARGS, note these restrictions:
- Required options: Always include
--foreground --user nobodyfor proper container operation and security - Conflicting options: Don't use
--listenas it conflicts with--foreground(required for containers) - Security: Avoid changing
--userfromnobodyas this breaks the container's security model - Directory: The TFTP root directory is fixed to
/srv/tftpand cannot be changed via arguments
Note: Setting up host directory permissions for TFTP uploads is beyond the scope of this README, as requirements vary by environment.
For general guidance when using --create: the container process needs write access to the mounted directory. This typically involves setting appropriate permissions on the host directory before starting the container.