LinuxServer.io–style containerized server that coordinates Mode S multilateration across many mlat-client receivers.
Unofficial community image for mlat-server, built with LinuxServer.io style container patterns (s6, hardened defaults, practical runtime options) for ADS-B multilateration workloads.
Sponsored and maintained by Blackout Secure.
Important This repository is not an official LinuxServer.io image release.
This project packages upstream wiedehopf/mlat-server into an easy-to-run, LinuxServer.io-style container image with practical defaults for running your own Mode S multilateration coordinator.
The mlat-server is the server side of the multilateration protocol: many mlat-client instances forward timestamped Mode S messages from their receivers to this server, which correlates the signals and resolves aircraft positions geometrically.
AGPL-3.0 Network Use Notice mlat-server is licensed under the GNU Affero GPL v3. If you operate a modified copy of this server and allow third parties to connect to it over a network, AGPL Section 13 requires you to provide those users with a way to obtain the complete corresponding source code of your modifications. See LICENSE for the full text.
Quick links:
- Docker Hub listing: blackoutsecure/mlat-server
- Balena block listing: mlat-server block on Balena Hub
- GitHub repository: blackoutsecure/docker-mlat-server
- Upstream application: wiedehopf/mlat-server
- Quick Start
- Image Availability
- About The mlat-server Application
- Supported Architectures
- Network Topology
- Usage
- Parameters
- Configuration
- Application Setup
- Troubleshooting
- Release & Versioning
- Development & Publishing
- License
- Support & Getting Help
- References
5-minute multilateration server setup:
docker run -d \
--name=mlat-server \
--restart unless-stopped \
-e TZ=Etc/UTC \
-e MLAT_SERVER_CLIENT_LISTEN=31090 \
-e MLAT_SERVER_FILTERED_BASESTATION_LISTEN=31003 \
-e MLAT_SERVER_BASESTATION_LISTEN=31004 \
-e MLAT_SERVER_WORK_DIR=/config/work \
-e MLAT_SERVER_MOTD="Welcome to my MLAT server" \
-p 31090:31090/tcp \
-p 31090:31090/udp \
-p 31003:31003/tcp \
-p 31004:31004/tcp \
-v mlat-server-config:/config \
blackoutsecure/mlat-server:latestFor compose files, balena, and more examples, see Usage below.
Docker Hub (Recommended):
- All images published to Docker Hub
- Simple pull command:
docker pull blackoutsecure/mlat-server:latest - Multi-arch support: amd64, arm64
- No registry prefix needed (defaults to Docker Hub)
# Pull latest
docker pull blackoutsecure/mlat-server:latest
# Pull a specific commit-pinned build
docker pull blackoutsecure/mlat-server:0.0.0-g9b27a6dNote on versioning: Upstream
wiedehopf/mlat-serverdoes not maintain aversionfile or git tags. This image synthesizes a PEP 440-style local version from the upstream commit short SHA (e.g.0.0.0+g9b27a6d), and publishes a Docker-tag-safe variant (+→-).
mlat-server is a Mode S multilateration server. It accepts connections from many mlat-client feeders, ingests their synchronized Mode S timing data, and resolves transmitter positions by multilateration.
Author and maintenance credits (upstream):
- Original author: Oliver Jowett (mutability)
- Current upstream maintainer: wiedehopf (Matthias Wirth)
- Additional contributor: TanerH
- Upstream repository and documentation: wiedehopf/mlat-server
This image is published as a multi-arch manifest. Pulling
blackoutsecure/mlat-server:latest retrieves the correct image for your host
architecture.
The architectures supported by this image are:
| Architecture | Tag |
|---|---|
| x86-64 | amd64-latest |
| arm64 | arm64v8-latest |
The mlat-server listens on a set of ports; it does not connect outbound to receivers. Mode S timing data flows into the server, and resolved aircraft positions flow out of the server in Basestation/SBS format.
┌──────────────────────────────────┐
mlat-client ──► │ 31090/tcp + 31090/udp │
mlat-client ──► │ (--client-listen) │
mlat-client ──► │ │
│ mlat-server │
│ │
│ 31003/tcp ──► SBS (filtered) ──►│ to glue/MQTT/etc.
│ 31004/tcp ──► SBS (raw) ───────►│ to local consumers
└──────────────────────────────────┘
| Port | Proto | Purpose |
|---|---|---|
| 31090 | tcp | mlat-client primary connection (control, sync) |
| 31090 | udp | mlat-client message stream (lower latency, optional) |
| 31003 | tcp | Basestation/SBS output — Kalman-filtered positions |
| 31004 | tcp | Basestation/SBS output — unfiltered positions |
UDP for --client-listen is optional; if you do not publish 31090/udp, the
client will fall back to TCP-only.
docker-compose (recommended, click here for more info)
---
services:
mlat-server:
image: blackoutsecure/mlat-server:latest
container_name: mlat-server
environment:
- TZ=Etc/UTC
- MLAT_SERVER_CLIENT_LISTEN=31090
- MLAT_SERVER_FILTERED_BASESTATION_LISTEN=31003
- MLAT_SERVER_BASESTATION_LISTEN=31004
- MLAT_SERVER_WORK_DIR=/config/work
- MLAT_SERVER_TAG=mlat-server
- MLAT_SERVER_STATUS_INTERVAL=15
- MLAT_SERVER_MOTD=Welcome to this Mode S multilateration server
- LOG_LEVEL=info # debug | info | warn | error | fatal
ports:
- 31090:31090/tcp
- 31090:31090/udp
- 31003:31003/tcp
- 31004:31004/tcp
volumes:
- mlat-server-config:/config
tmpfs:
- /tmp
- /run:exec
restart: unless-stopped
volumes:
mlat-server-config:---
services:
mlat-server:
image: blackoutsecure/mlat-server:latest
container_name: mlat-server
environment:
- TZ=Etc/UTC
- MLAT_SERVER_CLIENT_LISTEN=31090
- MLAT_SERVER_FILTERED_BASESTATION_LISTEN=31003
- MLAT_SERVER_BASESTATION_LISTEN=31004
- MLAT_SERVER_WORK_DIR=/config/work
- MLAT_SERVER_MOTD=Private MLAT network
- LOG_LEVEL=info
ports:
- 31090:31090/tcp
- 31090:31090/udp
- 31003:31003/tcp
- 31004:31004/tcp
volumes:
- mlat-server-config:/config
tmpfs:
- /tmp
- /run:exec
restart: unless-stopped
mlat-client:
image: blackoutsecure/mlat-client:latest
container_name: mlat-feeder
depends_on:
- mlat-server
environment:
- TZ=Etc/UTC
- MLAT_CLIENT_INPUT_CONNECT=readsb:30005
- MLAT_CLIENT_SERVER=mlat-server:31090
- MLAT_CLIENT_LAT=51.5
- MLAT_CLIENT_LON=-0.1
- MLAT_CLIENT_ALT=50m
- MLAT_CLIENT_USER_ID=local-receiver-1
- MLAT_CLIENT_RESULTS=beast,connect,readsb:30104
- LOG_LEVEL=info
volumes:
- mlat-client-config:/config
tmpfs:
- /tmp
- /run:exec
restart: unless-stopped
volumes:
mlat-server-config:
mlat-client-config:Note: When both services are in the same compose file, Docker Compose creates a shared network automatically — the client can reach the server at
mlat-server:31090. If you run mlat-server and mlat-client from separate compose files or hosts, expose 31090/tcp+udp via published ports and have the client connect to<host>:31090.
docker-cli (click here for more info)
docker run -d \
--name=mlat-server \
-e TZ=Etc/UTC \
-e MLAT_SERVER_CLIENT_LISTEN=31090 \
-e MLAT_SERVER_FILTERED_BASESTATION_LISTEN=31003 \
-e MLAT_SERVER_BASESTATION_LISTEN=31004 \
-e MLAT_SERVER_WORK_DIR=/config/work \
-e MLAT_SERVER_MOTD="Welcome to this Mode S multilateration server" \
-p 31090:31090/tcp \
-p 31090:31090/udp \
-p 31003:31003/tcp \
-p 31004:31004/tcp \
-v /path/to/mlat-server/config:/config \
--restart unless-stopped \
blackoutsecure/mlat-server:latestThis image can be deployed to Balena-powered IoT devices using the included
docker-compose.yml file (which contains the required Balena labels):
balena push <your-app-slug>For deployment via the web interface, use the deploy button in this repository. See Balena documentation for details.
| Parameter | Description | Example |
|---|---|---|
-e MLAT_SERVER_CLIENT_LISTEN= |
Listening port(s) for mlat-client connections — tcp_port, host:tcp_port, tcp_port:udp_port, or host:tcp_port:udp_port; multiple endpoints comma-separated |
31090 |
-e MLAT_SERVER_WORK_DIR= |
Directory for sync.json, aircraft.json, blacklist, and other runtime state | /config/work |
| Parameter | Default | Description |
|---|---|---|
-e TZ=Etc/UTC |
Etc/UTC |
Timezone (TZ database) |
-e MLAT_SERVER_MOTD= |
(none) | Message of the day sent to clients on connect |
-e MLAT_SERVER_BASESTATION_LISTEN= |
(none) | Listen on [host:]port for unfiltered Basestation/SBS results |
-e MLAT_SERVER_BASESTATION_CONNECT= |
(none) | Connect to host:port and push unfiltered SBS results |
-e MLAT_SERVER_FILTERED_BASESTATION_LISTEN= |
(none) | Listen on [host:]port for Kalman-filtered SBS results |
-e MLAT_SERVER_FILTERED_BASESTATION_CONNECT= |
(none) | Connect to host:port and push Kalman-filtered SBS results |
-e MLAT_SERVER_WRITE_CSV= |
(none) | Write resolved positions to a CSV file (e.g. /config/work/positions.csv) |
-e MLAT_SERVER_DUMP_PSEUDORANGE= |
(none) | Dump pseudorange data in JSON format to a file |
-e MLAT_SERVER_PARTITION= |
1/1 |
Cluster partitioning, form I/N |
-e MLAT_SERVER_TAG= |
mlat-server |
Process name prefix (requires setproctitle) |
-e MLAT_SERVER_STATUS_INTERVAL= |
15 |
Status logging interval in seconds (multiple of 15, -1 to disable) |
-e MLAT_SERVER_CHECK_LEAKS= |
false |
Enable periodic objgraph memory-leak checks |
-e MLAT_SERVER_ARGS= |
(none) | Raw arguments passed directly to mlat-server (overrides individual env vars) |
-e MLAT_SERVER_USER= |
root |
Runtime user for the container |
-e PUID=911 |
911 |
User ID for non-root operation |
-e PGID=911 |
911 |
Group ID for non-root operation |
| Volume | Description | Required |
|---|---|---|
-v /config |
Configuration, work directory, and persistent state | Required |
The image sets numerical-library thread limits to prevent runaway BLAS threads from oversubscribing CPU cores. These are baked into the image and generally do not need adjustment:
MKL_NUM_THREADS=1
NUMEXPR_NUM_THREADS=1
OMP_NUM_THREADS=1
PYTHONOPTIMIZE=2
Environment variables are set using -e flags in docker run or the
environment: section in docker-compose.
You can configure mlat-server in two ways:
-
Individual environment variables (recommended): Set
MLAT_SERVER_CLIENT_LISTEN,MLAT_SERVER_WORK_DIR, etc. The container service script assembles the command-line arguments. -
Raw arguments: Set
MLAT_SERVER_ARGSto the full argument string. This takes precedence — individual env vars that match flags already present inMLAT_SERVER_ARGSare skipped.
mlat-server emits resolved aircraft positions in
Basestation/SBS-1
format, the de-facto interchange format used by readsb, dump1090, Virtual
Radar Server, FlightAirMap, tar1090, Plane/Ship Plotter, and many others.
| Output | Filtering |
|---|---|
--filtered-basestation-listen (MLAT_SERVER_FILTERED_BASESTATION_LISTEN) |
Kalman-filtered (smoothed) |
--basestation-listen (MLAT_SERVER_BASESTATION_LISTEN) |
Raw, unfiltered |
You can additionally write a CSV trail of resolved positions with
MLAT_SERVER_WRITE_CSV=/config/work/positions.csv.
For high-load deployments, mlat-server supports horizontal partitioning. Set
MLAT_SERVER_PARTITION=I/N where N is the total number of partitions and
I is this server's index (1-based). Each partition handles a deterministic
slice of incoming aircraft, allowing the work to be split across multiple
processes or hosts.
By default, this container runs as root.
Root mode (default):
- No
PUIDorPGIDneeded
Non-root mode (advanced):
- Set
MLAT_SERVER_USERto your username - Provide matching
PUIDandPGIDvalues - Defaults to
911:911if omitted
The container runs mlat-server, accepting timestamped Mode S messages from mlat-client feeders and resolving aircraft positions via multilateration.
- Multilateration coordinator: Resolves transmitter positions by correlating timestamps from three or more synchronized receivers
- High concurrency: Uses uvloop and asyncio for thousands of simultaneous client connections
- SBS output: Filtered (Kalman) and raw Basestation/SBS streams
- CSV logging: Optional persistent CSV trail
- Cluster partitioning: Built-in
I/Nsharding for horizontal scale-out
Common environment variable combinations:
# Public server with both filtered and raw SBS feeds
-e MLAT_SERVER_CLIENT_LISTEN=31090
-e MLAT_SERVER_FILTERED_BASESTATION_LISTEN=31003
-e MLAT_SERVER_BASESTATION_LISTEN=31004
# Private server with persistent CSV log
-e MLAT_SERVER_CLIENT_LISTEN=31090
-e MLAT_SERVER_WRITE_CSV=/config/work/positions.csv
# Pushing SBS results to a downstream consumer
-e MLAT_SERVER_FILTERED_BASESTATION_CONNECT=tar1090:30003
# Partitioned cluster member 2 of 4
-e MLAT_SERVER_PARTITION=2/4
-e MLAT_SERVER_TAG=mlat-server-shard2For all available options, see the mlat-server documentation.
Check logs:
docker logs mlat-server
docker logs mlat-server --tail 50 -f # Follow last 50 linesCommon causes:
- Missing required arguments: Ensure
MLAT_SERVER_CLIENT_LISTENandMLAT_SERVER_WORK_DIRare set - Port conflict: Another service is already bound to one of 31003 / 31004 / 31090 on the host
- Permission error on work directory: When running non-root, ensure the
mounted
/configis writable byPUID:PGID - Configuration syntax error: Check
MLAT_SERVER_ARGS
Verify the client-listen port is reachable:
docker exec mlat-server nc -zv 127.0.0.1 31090
# from a client host:
nc -zv <docker-host> 31090If connection fails:
- Confirm
-p 31090:31090/tcp(and optionally/udp) are published - Check host firewall rules
- Verify clients are pointing at the correct host:port and not blocked upstream (NAT, security group, etc.)
If clients connect but the server does not produce positions:
- MLAT requires 3 or more synchronized receivers seeing the same aircraft simultaneously — verify multiple clients are connected and reporting fresh data
- Each client's reported lat/lon/alt must be accurate (sub-100m for best results)
- Inspect the periodic status log line (
MLAT_SERVER_STATUS_INTERVAL) for sync diagnostics - Check
${MLAT_SERVER_WORK_DIR}/sync.jsonfor per-pair clock-sync state
- Check upstream mlat-server documentation
- Review container logs:
docker logs -f mlat-server - Open an issue on GitHub
This project uses synthetic versioning derived from the upstream mlat-server
commit (since upstream maintains no version file or tags):
- Image tag pattern:
0.0.0-g<short_sha>(e.g.0.0.0-g9b27a6d) pluslatest - Releases published on GitHub Releases
- Multi-arch images (amd64, arm64v8) built automatically
- An
upstream-release-monitorworkflow opens a PR whenever upstream HEAD changes
Update to latest:
docker pull blackoutsecure/mlat-server:latest
docker compose up -d # if using composeCheck image version:
docker inspect -f '{{ index .Config.Labels "build_version" }}' \
blackoutsecure/mlat-server:latestClone blackoutsecure/docker-mlat-server and validate from the repository root:
docker compose config --quiet
docker build --check .
actionlint .github/workflows/*.yml
shellcheck --shell=bash --external-sources \
root/etc/s6-overlay/s6-rc.d/*/run root/usr/local/bin/*.sh
docker build -t blackoutsecure/mlat-server:dev .Keep the files under root/ LF-terminated, including the extensionless s6
scripts. The Git attributes enforce this on Windows checkouts.
The virtual environment retains setuptools<82 because python-graph-core
still imports pkg_resources. The build runs mlat-server --help to catch
missing application imports before producing an image.
Use dev for development and promote changes to main through a pull
request. The publish workflow builds pull requests and pushes to dev
without registry credentials. Pushes to main publish
blackoutsecure/mlat-server; manual runs are build-only unless the
publish input is enabled. Changes to the upstream tracker also trigger
builds, so merging an upstream-update PR rebuilds the image. Build metadata
uses the same synthetic upstream version as the registry tags.
Configure Actions before publishing from a newly migrated repository:
DOCKERHUB_USERNAMEandDOCKERHUB_TOKENsecrets are required, with write access to the Docker Hub namespace.DOCKERHUB_NAMESPACEis an optional repository variable, with the same-named secret supported for compatibility. It defaults toblackoutsecure; override it only when publishing to another namespace.- Balena publishing is optional. Configure both the
BALENA_API_TOKENsecret andBALENA_NAMESPACEvariable (or legacyBALENA_USERNAMEsecret) to enable it. Neither configured explicitly skips Balena; a partial configuration fails preflight. - The upstream monitor opens pull requests using
GITHUB_TOKEN. Enable Allow GitHub Actions to create and approve pull requests in the repository's Actions workflow permissions, subject to organization policy. Upstream updates still require review and merging.
Repository credentials and settings are not copied from the old repository. The image must be published in the new namespace before the pull examples above can be used there.
This project (the Dockerfile, scripts, and packaging) and the upstream
mlat-server are both licensed under the GNU Affero General Public License
v3.0 or later (AGPL-3.0-or-later). See the LICENSE file for the
full text.
AGPL Section 13 (Network Use): If you run a modified version of this server and let third parties interact with it over a network, you must offer those users access to the corresponding source code of your modifications. Hosting an unmodified upstream build does not change your obligations beyond ordinary AGPL compliance.
The mlat-server application is © 2014 Oliver Jowett, with subsequent contributions by Matthias Wirth (wiedehopf), TanerH, and others. See wiedehopf/mlat-server for upstream authorship and history.
- Questions: GitHub Issues
- Bug Reports: Include Docker version, container logs, and reproduction steps
- Upstream Documentation: mlat-server on GitHub
- Community: LinuxServer.io Discord
Get help:
docker logs mlat-server # View container logs
docker exec -it mlat-server /bin/bash # Access container shell
docker inspect blackoutsecure/mlat-server # Check image detailsSponsored and maintained by Blackout Secure
Upstream project: wiedehopf/mlat-server Container patterns: LinuxServer.io
| Resource | Link |
|---|---|
| Docker Hub | blackoutsecure/mlat-server |
| Balena Hub | mlat-server block |
| GitHub Issues | Report bugs or request features |
| GitHub Releases | Download releases |
| Resource | Link |
|---|---|
| mlat-server | wiedehopf/mlat-server |
| mlat-client | wiedehopf/mlat-client |
| docker-mlat-client | blackoutsecure/docker-mlat-client |
| readsb | wiedehopf/readsb |
| LinuxServer.io | linuxserver.io |
Made with ❤️ by Blackout Secure
