# Build
docker build -t amftpd .
# Run (maps host port 21 → container 2121)
docker run -d --name amftpd \
-p 21:2121 \
-p 50000-50100:50000-50100 \
-v $(pwd)/config:/config:ro \
-v $(pwd)/data:/data \
amftpdThe container expects:
/config/amftpd.json— your configuration file (read-only mount)/data/— writable directory for user DB, dupe DB, logs, ACME account key
Use the bundled deploy/docker-compose.yml for a full production setup:
# Start
docker compose -f deploy/docker-compose.yml up -d
# Follow logs
docker compose -f deploy/docker-compose.yml logs -f
# REHASH (reload config without restarting)
docker compose -f deploy/docker-compose.yml kill --signal SIGHUP amftpd
# Validate config
docker compose -f deploy/docker-compose.yml exec amftpd \
/app/amFTPd /config/amftpd.json --validateThe production Dockerfile and Compose file include several hardening measures:
Non-root user — the daemon runs as UID/GID 1500 (amftpd).
Ensure your host bind-mount directories are readable/writable by UID 1500:
sudo chown -R 1500:1500 ./data ./site
chmod 750 ./configRead-only root filesystem — read_only: true in docker-compose.yml prevents
any writes to the container filesystem. Only /tmp (tmpfs) and the mounted volumes
are writable.
Capability restrictions — cap_drop: ALL removes every Linux capability.
Add CAP_NET_BIND_SERVICE if you configure amFTPd to listen on port 21 directly
inside the container (rather than mapping host 21 → container 2121).
SIGTERM / graceful drain — stop_signal: SIGTERM and stop_grace_period: 75s
give in-progress transfers up to 60 seconds to complete before Docker sends SIGKILL.
| Service | Container port | Recommended host mapping |
|---|---|---|
| FTP control | 2121/tcp | 21:2121 |
| FTPS implicit | 990/tcp | 990:990 |
| Passive data range | 50000–50100 | 50000-50100:50000-50100 |
| ACME HTTP-01 | 80/tcp | 80:80 (only if ACME enabled) |
| Status / metrics | 8080/tcp | 8080:8080 (optional) |
The passive port range must match Server.PassivePortMin / Server.PassivePortMax
in amftpd.json.
To use automatic TLS certificate provisioning, the ACME HTTP-01 challenge server must be reachable on port 80 from the internet.
# docker-compose.yml excerpt
ports:
- "80:80" # ACME HTTP-01
environment:
- TZ=UTC # recommended for consistent log timestampsThe ACME manager writes the renewed PFX to /data/amftpd-cert.pfx and triggers
an internal REHASH to hot-swap the certificate without container restart.
# Self-contained Linux x64 (no .NET runtime on host needed)
docker build \
--build-arg PUBLISH_ARGS="--runtime linux-x64 --self-contained true" \
-t amftpd:standalone .
# Development image (skips trimming, includes debug symbols)
docker build --target build -t amftpd:dev .