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.
Contents
- What you need
- Your station values
- Prepare the Raspberry Pi
- Install Ultrafeeder
- Check it’s working
- Turn on MLAT
- Feed the OpenSky Network
- Everyday commands
- Troubleshooting
- Antenna tips
- FAQ
1. What you need
- Raspberry Pi 3B+, 4 or 5, with a good power supply. An underpowered Pi causes random dropouts.
- microSD card, 32 GB or more, preferably a high-endurance one.
- RTL-SDR USB dongle, ideally one made for 1090 MHz with a built-in filter and amplifier.
- 1090 MHz antenna and coax cable. Keep the cable short; long thin cable loses much of the signal.
- A computer on the same network to set it up from.
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.
| Setting | Example in this guide | Where it comes from |
|---|---|---|
| Compose file | /opt/adsb/docker-compose.yml | Created in step 3 |
| Time zone | America/Chicago | Your tz database name |
| Latitude, longitude | 39.82830, -98.57950 | Your antenna, from any map app. 5 decimals for MLAT |
| Antenna altitude | 566 m | Ground elevation plus antenna height, above sea level |
| Dongle serial | 00000001 | Factory default on most dongles (step 4) |
| UUID | your-uuid-here | Generated in step 3 |
| OpenSky username and serial | your-opensky-username, your-opensky-serial | Step 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
READSB_RTLSDR_DEVICEmust match your dongle’s serial number exactly. Most dongles ship as00000001. With a single dongle you can delete the line instead.READSB_GAIN=autouses readsb’s built-in automatic gain, which adjusts continuously to strong nearby and weak distant signals.READSB_ALTis the antenna’s height above sea level, withm(orft) at the end.ULTRAFEEDER_CONFIGlists where your data goes, one entry per line, with;after every entry except the last. To feed other networks too, add their entries the same way.MLAT_USERis the name shown for your station on the planedata.io contributors page. Use letters, numbers,-and_only.READSB_RX_LOCATION_ACCURACY:2shows your exact location on your own map page;1rounds it to 2 decimals if you’ll share that page with others.
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:
http://<pi-ip>:8080/: your live maphttp://<pi-ip>:8080/graphs1090/: message rate, range and signal graphshttp://<pi-ip>:8080?heatmap&realheat: a 24-hour heatmap of what you’ve received
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:
- Set
READSB_LATandREADSB_LONto your antenna’s position with 5 decimal places (about 1 metre), andREADSB_ALTto ground elevation plus antenna height. - 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 - Apply it with
docker compose up -d. - After an hour or so,
docker logs ultrafeedershows 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
LAT,LONGandALTare required; the container won’t start without them.ALTis in metres with no unit, unlike Ultrafeeder’sREADSB_ALT=566m.BEASTHOST=ultrafeederworks because both containers are in the same compose file and share its network.
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:
| Task | Command |
|---|---|
| Apply changes after editing the compose file | docker compose up -d |
| Restart a container with new settings | docker compose up -d --force-recreate ultrafeeder |
| See running containers | docker ps |
| Watch logs | docker logs -f ultrafeeder · docker logs -f opensky |
| Update to newer versions | docker compose pull && docker compose up -d |
| Stop everything | docker compose down |
| Remove a container you deleted from the file | docker 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
- Check the address is
feed.planedata.ioand the entry ends withbeast_reduce_plus_out. - Check
docker logs ultrafeeder | grep -i planedatafor connection errors. - Open the contributors page from the same network as the receiver: it recognises you by your internet address.
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
- Height and a clear view of the sky matter most. ADS-B is line of sight: a roof or attic beats a window, and a window beats a desk.
- Keep the coax short, or use low-loss cable. At 1090 MHz, several metres of thin cable can lose half the signal.
- Use a filtered dongle or a 1090 MHz filter if you live near mobile phone towers or broadcast transmitters.
- Watch the range graph in
graphs1090after each change to see what helped.
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.