planedata.io

How to build an ADS-B receiver with a Raspberry Pi and RTL-SDR

Track aircraft overhead with your own receiver: a live map on your network, statistics, and data shared with planedata.io and the OpenSky Network. This guide uses Docker with Ultrafeeder (decoder, map and feeds) and the OpenSky feeder container, and covers the problems people most often run into.

Prefer a ready-made image? The free adsb.im feeder image does most of this through a web page, with no command line. Flash it, then follow the adsb.im steps on our feeding page. This guide is for people who want to run the Docker containers themselves.

Contents

  1. What you need
  2. Your station values
  3. Prepare the Raspberry Pi
  4. Install Ultrafeeder
  5. Check it’s working
  6. Turn on MLAT
  7. Feed the OpenSky Network
  8. Everyday commands
  9. Troubleshooting
  10. Antenna tips
  11. FAQ

1. What you need

Expect around US$100 to $150 in parts if you’re starting from scratch. Setup takes about an hour.

2. Your station values

Collect these first. They identify your station, so keep your own copy private and don’t paste it into forums. The examples in this guide use a placeholder location in the middle of the United States (near Lebanon, Kansas); replace every value with your own.

SettingExample in this guideWhere it comes from
Compose file/opt/adsb/docker-compose.ymlCreated in step 3
Time zoneAmerica/ChicagoYour tz database name
Latitude, longitude39.82830, -98.57950Your antenna, from any map app. 5 decimals for MLAT
Antenna altitude566 mGround elevation plus antenna height, above sea level
Dongle serial00000001Factory default on most dongles (step 4)
UUIDyour-uuid-hereGenerated in step 3
OpenSky username and serialyour-opensky-username, your-opensky-serialStep 7

3. Prepare the Raspberry Pi

Flash the SD card and connect

Use Raspberry Pi Imager to write Raspberry Pi OS Lite (64-bit) to the card. In the OS customisation settings, set a username and password, your Wi-Fi if you’re not using a cable, and on the Services tab turn on Enable SSH. Boot the Pi, then from your computer (Terminal on Mac or Linux, PowerShell on Windows):

ssh [email protected]
# or with the Pi's IP address from your router:
ssh [email protected]

Install Docker and free up the SDR dongle

The SDR-Enthusiasts install script sets up Docker and Docker Compose, adds you to the docker group, adds the device rules for RTL-SDR dongles, and stops the TV-tuner drivers from claiming the dongle:

bash <(wget -q -O - https://raw.githubusercontent.com/sdr-enthusiasts/docker-install/main/docker-install.sh)

When it asks about excluding the SDR/DVB-T drivers, answer yes. This only frees the dongle for radio use; it doesn’t send data anywhere. Then reboot:

sudo reboot

Create the folder and a UUID

sudo mkdir -p -m 777 /opt/adsb
cd /opt/adsb
cat /proc/sys/kernel/random/uuid

Note the UUID it prints: it identifies your station to the networks you feed. The folder name matters too, because Docker names the containers’ network after it (adsb_default).

4. Install Ultrafeeder

Ultrafeeder decodes 1090 MHz ADS-B from the dongle, serves the tar1090 map and graphs, and sends your data to every network listed in ULTRAFEEDER_CONFIG. Create the compose file:

cd /opt/adsb
nano docker-compose.yml

Paste this, replacing the example values, then save with Ctrl+O, Enter, and exit with Ctrl+X:

services:
  ultrafeeder:
    image: ghcr.io/sdr-enthusiasts/docker-adsb-ultrafeeder
    container_name: ultrafeeder
    hostname: ultrafeeder
    restart: unless-stopped
    device_cgroup_rules:
      - "c 189:* rwm"
    ports:
      - 8080:80
    environment:
      # ---------- General ----------
      - LOGLEVEL=error
      - TZ=America/Chicago
      # ---------- SDR ----------
      - READSB_DEVICE_TYPE=rtlsdr
      - READSB_RTLSDR_DEVICE=00000001
      - READSB_GAIN=auto
      # ---------- Location (antenna) ----------
      - READSB_LAT=39.82830
      - READSB_LON=-98.57950
      - READSB_ALT=566m
      - READSB_RX_LOCATION_ACCURACY=2
      - READSB_STATS_RANGE=true
      # ---------- Feeds ----------
      - ULTRAFEEDER_CONFIG=
          adsb,feed.planedata.io,30004,beast_reduce_plus_out;
          mlat,feed.planedata.io,31090,39010
      - UUID=your-uuid-here
      - MLAT_USER=your-station-name
      # ---------- Map ----------
      - UPDATE_TAR1090=true
      - TAR1090_PAGETITLE=your-station-name
      - TAR1090_MESSAGERATEINTITLE=true
      - TAR1090_PLANECOUNTINTITLE=true
      - TAR1090_ENABLE_AC_DB=true
      - TAR1090_SITESHOW=true
      - TAR1090_RANGE_OUTLINE_COLORED_BY_ALTITUDE=true
      - TAR1090_USEROUTEAPI=true
      - GRAPHS1090_DARKMODE=true
    volumes:
      - /opt/adsb/ultrafeeder/globe_history:/var/globe_history
      - /opt/adsb/ultrafeeder/graphs1090:/var/lib/collectd
      - /proc/diskstats:/proc/diskstats:ro
      - /dev/bus/usb:/dev/bus/usb:rw
    tmpfs:
      - /run:exec,size=256M
      - /tmp:size=128M
      - /var/log:size=32M

What the important settings do

MLAT needs your exact location. If you don’t want to enter it to 5 decimal places with the correct antenna altitude, remove the mlat,… line (and the ; before it). A wrong location gives bad MLAT results and degrades them for nearby feeders too. See step 6.

Start it

cd /opt/adsb
docker compose up -d ultrafeeder
docker logs -f ultrafeeder

Press Ctrl+C to stop watching the logs; the container keeps running.

5. Check it’s working

docker ps                                    # ultrafeeder should be "Up (healthy)"
docker logs ultrafeeder | grep -i planedata  # the connection to planedata.io
docker exec -it ultrafeeder viewadsb         # live list of aircraft (Ctrl+C to exit)

On your own network:

Then open the planedata.io contributors page from a device on the same network: under Am I feeding? it should show your station with its message rate. It can take a minute or two after a restart.

6. Turn on MLAT

Multilateration (MLAT) locates aircraft that don’t broadcast their position, by comparing when several receivers heard the same signal. To take part:

  1. Set READSB_LAT and READSB_LON to your antenna’s position with 5 decimal places (about 1 metre), and READSB_ALT to ground elevation plus antenna height.
  2. Make sure the feed section has both lines:
          - ULTRAFEEDER_CONFIG=
              adsb,feed.planedata.io,30004,beast_reduce_plus_out;
              mlat,feed.planedata.io,31090,39010
  3. Apply it with docker compose up -d.
  4. After an hour or so, docker logs ultrafeeder shows mlat-client lines with the number of peers you’re synchronised with, and the contributors page shows your MLAT peers.

Your exact location goes only to the MLAT server, which needs it. To leave your name and area off the public contributors page, add ,--privacy to the end of the mlat,… entry.

7. Feed the OpenSky Network

OpenSky doesn’t go in ULTRAFEEDER_CONFIG. It runs as its own container that reads from Ultrafeeder’s Beast output (port 30005). Feeding OpenSky also raises your OpenSky API allowance.

Create an account and get a serial number

Sign up at opensky-network.org. OpenSky issues a serial number the first time a receiver connects, so run a temporary container for 60 seconds while Ultrafeeder is running:

timeout 60s docker run --rm -it --network adsb_default \
  -e LAT=39.82830 -e LONG=-98.57950 -e ALT=566 \
  -e BEASTHOST=ultrafeeder \
  -e OPENSKY_USERNAME=your-opensky-username \
  ghcr.io/sdr-enthusiasts/docker-opensky-network:latest

Look for a line like [SERIAL] Got a new serial number: -1234567890 and copy the number, including the minus sign. Keep it: reuse the same serial if you ever rebuild the Pi, otherwise OpenSky sees a new receiver every time. If adsb_default isn’t found, run docker network ls and use the network ending in _default.

Add OpenSky to the compose file

Inside services:, below the Ultrafeeder section:

  opensky:
    image: ghcr.io/sdr-enthusiasts/docker-opensky-network:latest
    container_name: opensky
    restart: unless-stopped
    depends_on:
      - ultrafeeder
    environment:
      - TZ=America/Chicago
      - BEASTHOST=ultrafeeder
      - LAT=39.82830
      - LONG=-98.57950
      - ALT=566
      - OPENSKY_USERNAME=your-opensky-username
      - OPENSKY_SERIAL=your-opensky-serial
    tmpfs:
      - /run:exec,size=64M
      - /var/log
cd /opt/adsb
docker compose up -d
docker logs opensky

Good signs: connected to 'collector.opensky-network.org', and a connection to ultrafeeder on port 30005 without repeating errors. Your receiver appears on your OpenSky profile after a while.

8. Everyday commands

Run these from /opt/adsb:

TaskCommand
Apply changes after editing the compose filedocker compose up -d
Restart a container with new settingsdocker compose up -d --force-recreate ultrafeeder
See running containersdocker ps
Watch logsdocker logs -f ultrafeeder · docker logs -f opensky
Update to newer versionsdocker compose pull && docker compose up -d
Stop everythingdocker compose down
Remove a container you deleted from the filedocker compose up -d --remove-orphans

Both containers use restart: unless-stopped, so they start again by themselves after a reboot or power cut.

9. Troubleshooting

“rtlsdr: no device matching … found”, and readsb restarts every few seconds

FATAL: rtlsdr: no device matching '1090' found.
  0:  Realtek, RTL2838UHIDIR, SN: 00000001
sdrOpen() failed, exiting!

The compose file asks for a dongle serial that doesn’t exist; the log lists the serial your dongle actually has (00000001 here). Set READSB_RTLSDR_DEVICE to that value, or delete the line, then:

cd /opt/adsb
grep RTLSDR_DEVICE docker-compose.yml          # confirm the edit was saved
docker compose up -d --force-recreate ultrafeeder
docker logs ultrafeeder 2>&1 | grep "invoked by" | tail -1

The end of that last line should show --device= with your serial, or no --device= at all. Planning a second dongle (for example 978 MHz UAT in the US)? Give each its own serial instead:

docker compose down
sudo apt install -y rtl-sdr        # if rtl_eeprom isn't installed
rtl_eeprom -s 1090                 # answer y, then unplug and replug the dongle
rtl_test                           # should show SN: 1090; Ctrl+C to stop

Then set READSB_RTLSDR_DEVICE=1090 and run docker compose up -d.

OpenSky: “Could not connect: Connection refused (111)”

[INPUT] Trying to connect to 'ultrafeeder': [172.18.0.2]:30005
[WARN] [INPUT] Could not connect: Connection refused (111)

Nothing is listening on Ultrafeeder’s port 30005, almost always because readsb in Ultrafeeder isn’t running (often the dongle problem above). OpenSky itself is fine: fix Ultrafeeder and OpenSky connects on its own.

Not showing on the contributors page

MLAT shows 0 peers

It takes up to an hour to synchronise, and needs other feeders within a few hundred kilometres who hear the same aircraft. Check your location and altitude are exact. Persistently bad sync usually means the location is wrong.

10. Antenna tips

11. FAQ

How much does an ADS-B receiver cost?

Around US$100 to $150 in parts: a Raspberry Pi, a microSD card, a power supply, a 1090 MHz RTL-SDR dongle and an antenna. A Pi you already own lowers that a lot.

Do I need MLAT?

No, but it helps. MLAT works out positions for aircraft that don’t broadcast them, together with nearby receivers. It needs your antenna’s exact location to within about 10 metres.

Can I feed several networks at once?

Yes. Ultrafeeder sends the same data to every network in ULTRAFEEDER_CONFIG, and OpenSky runs as a separate container reading from Ultrafeeder.

Is my location made public?

On planedata.io, the contributors page shows your area rounded to about 10 km and your MLAT name, or nothing if you choose privacy. Your exact location, IP address and UUID are never shown.

Does the receiver have to run all the time?

It’s most useful when it does, and a Raspberry Pi uses only a few watts. The containers restart automatically after a reboot or power cut.

Next steps

See your station on the contributors page, explore the live map, read how planedata.io works in the documentation, and use your higher feeder allowance with the API.