A minimal, secure TFTP server with optional web server based on Alpine Linux.
3.5K
A minimal, secure TFTP server with optional web server based on Alpine Linux, tftpd-hpa, and BusyBox httpd.
nobody user with proper privilege droppingThe same image is published to two registries:
docker pull kaczmar2/tftp-server # Docker Hub
docker pull ghcr.io/kaczmar2/tftp-server # GitHub Container Registry
Available tags:
latest — the most recent release1.4.1 — a fixed releaseFor a stable system, pin a version tag. The latest tag moves whenever the Alpine base image receives a security update.
Supported platforms: linux/amd64, linux/arm64, linux/arm/v7.
Note on the examples below: they use the GHCR name, because that is what the docker-compose.yml in the repository uses. Both registries carry identical images, so you can replace ghcr.io/kaczmar2/tftp-server with kaczmar2/tftp-server anywhere you see it.
# 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-server
ENABLE_WEBSERVER: Set to true to enable HTTP server, false for TFTP-only (default: false)WEB_PORT: TCP port for the HTTP server (default: 80). Only used when ENABLE_WEBSERVER=true.TZ: Timezone for logs and timestamps (default: UTC)TFTP_ARGS: Custom TFTP daemon arguments (see Custom TFTP Options section)Note: The container uses host networking, so WEB_PORT is the port on the host. The
supplied .env.example sets WEB_PORT=8080 to avoid a conflict with an existing web server
on port 80. Change it to 80 if port 80 is free. The examples in this document assume port 80.
docker compose up): TFTP + Web server modetftp-only (docker compose --profile tftp-only up): TFTP-only modeservices:
# 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 (default)
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
- WEB_PORT=${WEB_PORT:-80}
volumes:
- ${TFTP_ROOT:-/srv/docker/tftp}:/srv/tftp
- ${WEB_ROOT:-/srv/docker/www}:/srv/www
The tftp-web service has no profiles key, so docker compose up starts it. The
tftp-only service starts only when you ask for its profile.
/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-hpa
Test 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:
::ffff:10.10.10.144) or IPv6 address49118)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 ErrorLimitations:
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 (always required):
Web server (when ENABLE_WEBSERVER=true):
WEB_PORT must be accessible (default: 80)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-server
Add to your .env file:
# Enable write access with custom settings
TFTP_ARGS=--foreground --secure --create --verbosity 4 --user nobody
See the tftpd man page for all available options.
When customizing TFTP_ARGS, note these restrictions:
--foreground --user nobody for proper container operation and security--listen as it conflicts with --foreground (required for containers)--user from nobody as this breaks the container's security model/srv/tftp and cannot be changed via argumentsNote: Setting up host directory permissions for TFTP uploads is beyond the scope of this document, 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.
MIT. See the LICENSE file.
Content type
Image
Digest
sha256:8611caa3b…
Size
4.4 MB
Last updated
17 days ago
docker pull kaczmar2/tftp-server