Long-term statistics for your AIS-catcher station — the SkyStats idea, for ships.
AIS-catcher already shows a live map. AISSeaStats keeps the history and turns it into one simple page:
- Vessels seen per hour, day and month, split by distance (under 20 NM, 20–50 NM, beyond); click a bar to list them, each with its distance band
- Propagation days flagged when the station hears much further than usual (tropospheric ducting), and the furthest vessels
- Top routes, inferred from where each vessel enters and leaves your coverage, drawn on a map
- Top vessels by passages, days seen, length, speed and distance, 10 to 100 per page
- Interesting vessels: military, authorities and rescue, superyachts, dangerous goods, very large ships, rare flags, your own watch list
- Fleet by type and flag, and range by direction, with the vessel and time of each record
- Regulars: vessels that come back most regularly, and the time between passages on each vessel card
- Busiest hours: average traffic by weekday and hour
- Station reception: hour-by-hour strip, share of hours with messages and interruptions
- A vessel card on click: photo (your own, or Wikimedia Commons), identity, size, first and last seen, recent passages, track, links to aiscatcher.org, VesselFinder and ShipSpotting
- Alerts when your station stops receiving: ntfy, Telegram, webhook or e-mail
French and English, light and dark themes, works on a phone. Runs next to AIS-catcher on a Raspberry Pi.
Français : voir docs/README.fr.md.
flowchart LR
A["AIS-catcher"] -- "HTTP POST JSON, every 15 s" --> B["ingest.php"]
B --> C[("MariaDB")]
W["worker"] --> C
C --> E["api.php"] --> F["Browser"]
AIS-catcher's built-in HTTP output (-H) posts decoded messages. AISSeaStats does not store every message: it updates counters, one sampled position per vessel per minute (kept 30 days by default), passages and range records. A year of statistics stays well under 1 GB.
| Minimum | Recommended | |
|---|---|---|
| Host | 64-bit Linux: Raspberry Pi 4 or 5 with Raspberry Pi OS 64-bit, or any amd64 machine | Pi 4 (2 GB+) or Pi 5 |
| Memory available | 512 MB | 1 GB |
| Free disk space | 1 GB | 2 GB, ideally on an SSD rather than the SD card |
| Docker | Docker Engine with the Compose plugin (docker compose) |
latest stable |
| AIS-catcher | 0.29 or newer, reachable over the network | latest |
32-bit systems (armv7l, armv6l) are not supported: the official MariaDB image is 64-bit only.
Check before installing:
uname -m # must print aarch64 or x86_64
docker compose version # must print a version
free -h # "available" column
df -h . # "Avail" columnNo Docker yet? curl -fsSL https://get.docker.com | sh, then sudo usermod -aG docker $USER and log in again.
install.sh runs these checks itself and stops with a clear message if something is missing.
git clone https://github.com/Crchlnn/AISSeaStats.git
cd AISSeaStats
./install.shinstall.sh downloads the ready-made image (64-bit Raspberry Pi and PC, about a minute), creates .env with random database passwords and starts three containers: db (MariaDB 11), app (lighttpd + PHP-FPM) and worker (background jobs). If the image cannot be downloaded, it builds it on your machine instead.
Prefer to build the image yourself? ./install.sh --build. Allow 5 to 10 minutes on a Raspberry Pi (about 6.5 minutes measured on a Pi 4), 1 to 2 minutes on a PC: PHP extensions are compiled once. Both ways give the same result and can be switched at any time (see Upgrade).
Then open http://<pi-address>:8095 and the setup wizard asks for:
- the station name and antenna position (used for distances and range, never sent anywhere),
- the time zone and language,
- an admin password.
If AIS-catcher runs in managed mode, the wizard can pre-fill the name and position from its config.json (read in your browser, never uploaded). It then shows your ingestion token and how to fill AIS-catcher's HTTP output.
Without Git, with the ready-made image only:
mkdir aisseastats && cd aisseastats
curl -fsSLO https://raw.githubusercontent.com/Crchlnn/AISSeaStats/main/docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/Crchlnn/AISSeaStats/main/.env.example -o .env
nano .env # set DB_PASSWORD and DB_ROOT_PASSWORD to long random values
docker compose up -dManual alternative from the cloned folder: cp .env.example .env, edit the passwords, then docker compose up -d (ready-made image) or docker compose up -d --build (built here).
ghcr.io/crchlnn/aisseastats, for linux/arm64 (Raspberry Pi 4/5 with a 64-bit OS) and linux/amd64, published automatically by GitHub Actions for each release:
| Tag | Follows |
|---|---|
latest |
the latest release (default) |
1.1 |
bug fixes of 1.1 only (1.1.2, 1.1.3…) |
1.1.2 |
exactly this version |
To pin a tag, set AISSEASTATS_VERSION=1.1 (for example) in .env. Database changes are applied when the container starts, so an automatic updater (Watchtower or similar) that pulls a new image and restarts the app and worker containers keeps working with your data. Updates then install without you looking: keep a backup (see Everyday commands), or pin a tag to choose when to move on.
In AIS-catcher: Output → HTTP → add an output, then:
| AIS-catcher field | Value |
|---|---|
| Description | anything, e.g. AISSeaStats |
| Link | empty |
| URL | http://<pi-address>:8095/ingest.php |
| Interval | 15 |
| ID | optional: station name, shown in the ingestion log |
| Credentials | aisseastats:<token> |
| Protocol | AISCATCHER (the default) |
| Gzip | on |
| Response | either way (on shows AISSeaStats' reply in the AIS-catcher log) |
| Unique / Downsample Position | off |
Keep Active on and save with the floppy-disk icon at the top right: while "You have unsaved changes" shows, nothing is applied.
Add these options to your existing AIS-catcher command (do not run the line on its own):
-M DTM -H http://<pi-address>:8095/ingest.php interval 15 gzip on userpwd aisseastats:<token> id MyStation
-M DTM adds signal level, reception time and flag country to each message. Field reference: AIS-catcher JSON decoding. Without it, the flag is derived from the MMSI and signal levels are not recorded.
AIS-catcher usually runs in its own Docker container, so localhost and 127.0.0.1 point to that container, not to AISSeaStats. Use:
- the Pi's IP address, e.g.
http://192.168.1.20:8095/ingest.php, or - its name from your local DNS (Pi-hole, router), without
.local, e.g.http://mypi:8095/ingest.php.
Names ending in .local (mDNS/Bonjour) usually work in a browser but not inside Docker containers.
The first figures appear within a minute. Routes appear once vessels have left your coverage for 2 hours.
docker compose exec app php bin/simulate.php --hours 72 # 72 h of fake traffic around your station
docker compose exec app php bin/reset-data.php --yes # remove it before real useYour statistics are kept: they live in the Docker volume aisseastats_db-data, and your settings in the database and in .env, none of which Git or the upgrade touch. Database changes are applied automatically when the app starts.
cd AISSeaStats # the folder you installed from
git pull # get the new version (docs, docker-compose.yml)
docker compose pull && docker compose up -d # ready-made image: download and restartor, to build the image on your machine as before:
cd AISSeaStats
git pull
docker compose up -d --build # rebuild and restartBoth ways can be used on the same station, one update with one, the next with the other: they give the image the same name and use the same data. An installation made before 1.1.2 built an image named aisseastats:local; it is no longer used and can be removed with docker image rm aisseastats:local.
Then reload the page (Ctrl+F5 / Cmd+Shift+R if the look did not change). Check the version at the bottom of the page and what changed in Version history.
- Backup first, if you want to be safe (see the table below): an upgrade does not delete data, but a backup costs nothing.
git pullrefuses to run ("your local changes would be overwritten"): you edited a tracked file.git stash, thengit pull, thengit stash popif you want your change back; your data is not affected.- Going back to a previous version: set
AISSEASTATS_VERSION=1.1.1(for example) in.envand rundocker compose pull && docker compose up -d, orgit checkout v1.1.1thendocker compose up -d --build. Database changes are not rolled back, so prefer restoring a backup taken before the upgrade. - Only
docker compose down -vdeletes the data (the-vremoves the volume).
Newest first. Details, fixes and database changes for each version: CHANGELOG.md (English) · docs/CHANGELOG.fr.md (français).
| Version | Date | What's new |
|---|---|---|
| 1.1.2 | 2026-10-10 | Ready-made image documented and used by default (docker compose pull), building it yourself still possible; Top vessels and Regulars: 10, 20, 50 or 100 per page with pages; vessel list of a chart bar: sort by distance and filter by distance band |
| 1.1.1 | 2026-10-10 | Vessel list of a chart bar: a coloured square for each vessel's distance band, its distance and the totals per band; range chart tooltip: vessel, date and time of each record; time of the record in the furthest vessels list |
| 1.1.0 | 2026-10-10 | Vessel chart split by distance band; exceptional propagation days and furthest vessels; Regulars and time between passages; busiest hours (weekday × hour); station reception strip |
| 1.0.0-beta.8 | 2026-10-06 | Debug capture also written to a dated JSON Lines file (contributed by @Phil353556); aiscatcher.org link on the vessel card instead of MarineTraffic, whose links no longer work |
| 1.0.0-beta.7 | 2026-09-30 | Debug capture of raw messages in the admin; explanation when a vessel's name has not been received; 30/90-day charts always show the whole period |
| 1.0.0-beta.6 | 2026-09-30 | Alerts when reception stops, and when it comes back: ntfy, Telegram, webhook (Discord, Slack, Gotify, Home Assistant…) and e-mail, with a test button; optional heartbeat URL to detect a station that is off; type of inland vessels from Inland AIS data; clearer database size in the admin |
| 1.0.0-beta.5 | 2026-09-30 | Destination dictionary in the admin (e.g. SAINT-MALO, ST-MALO, FR SML → FRSML); meaningless destinations (0, Q…) shown as Unknown; click a bar of the vessel chart to list its vessels; maximum range raised to 1,500 NM by default (tropospheric ducting), with far positions confirmed before they set a record |
| 1.0.0-beta.4 | 2026-09-30 | One period selector for the whole page, remembered; automatic refresh; click a type, flag, route or destination to list its vessels; readable fleet bars on phones; your own vessel photos, then Wikimedia Commons / Wikidata; setup pre-filled from AIS-catcher's config.json; fixes for map tiles "Access blocked" and empty charts |
| 1.0.0-beta.3 | 2026-09-29 | Flag derived from the MMSI; "rare flag" only once 100 vessels are known; clearer AIS-catcher instructions |
| 1.0.0-beta.2 | 2026-09-29 | Fix for "Invalid form token" in Docker; install time documented; memory and disk checks in install.sh |
| 1.0.0-beta.1 | 2026-09-29 | First test version |
| Task | Command |
|---|---|
| Upgrade (ready-made image) | git pull && docker compose pull && docker compose up -d |
| Upgrade (build here) | git pull && docker compose up -d --build |
| Logs | docker compose logs -f app worker |
| Status | docker compose ps |
| Lost admin password | docker compose exec app php bin/reset-admin.php |
| Backup | docker compose exec db sh -c 'mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" aisseastats' | gzip > aisseastats.sql.gz |
| Restore a backup | gunzip -c aisseastats.sql.gz | docker compose exec -T db sh -c 'mariadb -u root -p"$MARIADB_ROOT_PASSWORD" aisseastats' |
| Uninstall (keeps data) | docker compose down |
| Uninstall and delete data | docker compose down -v |
- Distance bands. Each vessel is counted once per hour, day or month, in the band of its furthest position: under 20 NM, 20–50 NM, 50 NM and beyond, or distance unknown (no position received, or hours recorded before 1.1.0).
- Vessel list of a bar. Clicking a bar lists its vessels with a coloured square for their band (same colours as the chart) and their furthest distance; the totals per band at the top count the whole bar, even when the list stops at 300 vessels. Sort by messages (by name for an hour) or by distance; click a band to list only its vessels, click it again to see them all.
- Range records. Hovering the range chart shows, for each 10° sector, the vessel that set the record and when. Records set before 1.1.1 get their time from the sampled positions when these are still kept (30 days); older ones show their day only.
- Exceptional propagation. A day is flagged (orange triangle above the bar) when its range is at least twice the usual range (median of the 30 previous days), at least 20 NM more, and at least 3 vessels were heard beyond 1.5 × the usual range. Beyond the horizon, VHF reception depends on tropospheric ducting, which follows the weather: see the forecast on dxinfocentre.com.
- Regulars. Time between passages is measured from the end of one passage to the start of the next (a passage ends after 2 hours without hearing the vessel). Vessels with at least 4 passages in the period are ranked by how steady that time is. A vessel moored at the edge of coverage, heard on and off, can count several short passages.
- Busiest hours. Average number of vessels per hour, by weekday and hour in the station's time zone, over the period (at least 7 days). Hours without any message are left out, so an outage does not look like calm.
- Station reception. One cell per hour: green when messages were received, red when none were. At a station that always hears a shore base station, red means an outage; in a quiet area it can also mean no traffic. To be warned of outages, see Alerts.
http://<pi-address>:8095/admin.php: ingestion health, station settings, rules for interesting vessels, named zones for routes, destination dictionary, alerts, vessel photos, token rotation, password, deletion of one vessel's data or of everything.
Named zones make routes readable. By default a route goes from one compass sector around the station to another (SW → NE). Add zones such as a lock, a port or a town and routes become Harbour → North lock. Existing passages are recomputed when zones change.
Destination dictionary. Crews type the destination freely: FRSML, FR SML, SAINT-MALO, ST-MALO… Group the spellings of a port under one name (for example SAINT-MALO, ST-MALO, FR SML → FRSML). The admin lists the destinations received in the last 90 days to help. Meaningless values such as 0 or Q are shown as Unknown.
Maximum plausible range (1,500 NM by default): tropospheric ducting can bring messages from over 1,000 NM. Beyond 50 NM, a position only counts for range records when the same vessel was received shortly before at a consistent position, so a single corrupted message cannot set a record.
AIS-catcher does not tell you when reception stops. AISSeaStats can: in the admin, Alerts sends one message when nothing has been received for N minutes (30 by default), and another when reception comes back. Channels, any combination:
- ntfy: the ntfy app on your phone, with the public ntfy.sh server or your own. Pick a topic name that is hard to guess.
- Telegram: a bot created with @BotFather, its token and your chat ID.
- Webhook: a JSON POST readable by Discord, Slack, Mattermost, Gotify, Home Assistant or n8n.
- E-mail: through your provider's SMTP server (STARTTLS or SSL/TLS).
“Send a test” checks each channel and shows the error if one fails. These alerts are sent by the Pi itself, so they cannot warn you if the Pi is off or offline: for that, give a heartbeat URL (healthchecks.io, Uptime Kuma in Push mode…), called every 5 minutes while reception works.
The vessel card shows, in this order:
- your own photo, added in the admin page (Vessel photos) or from the card's "Add a photo" link when signed in as admin; it is stored in the database;
- a free photo from Wikimedia Commons, found by IMO number (files categorised
IMO nnnnnnn); - the image of the matching Wikidata item, found by IMO number (P458) or MMSI (P587).
Free photos are shown with author and licence and cached 30 days. Many fishing boats and pleasure craft have no free photo anywhere: they get a silhouette, and you can add yours. VesselFinder, ShipSpotting and aiscatcher.org are linked, never fetched: their terms do not allow automated reuse of their photos. The Wikimedia/Wikidata look-up is the only outbound call and can be turned off in the admin.
| Symptom | Fix |
|---|---|
| "Waiting for the first data from AIS-catcher" | Check the HTTP output is saved and Active in AIS-catcher, and that its URL uses the Pi's IP or DNS name (not localhost, not .local). The admin page shows the last batch received and the last error. |
| Admin shows "token rejected" | The credentials in AIS-catcher do not match: generate a new token in the admin and paste aisseastats:<token> again. |
| Map tiles show "Access blocked" | Upgrade to 1.0.0-beta.4 or later (the page now sends the Referer that OpenStreetMap requires), or set another tile server in the admin. |
| A chart stays empty after switching period | Upgrade to 1.0.0-beta.4 or later, then reload the page. |
| A vessel has an MMSI but no name | The name only comes in the vessel's identity message (AIS type 5, or 24 for class B), sent every 6 minutes and longer than a position report, so it is the first lost at the edge of range. In the admin, Debug: received messages records what AIS-catcher sends for that MMSI and shows which message types arrive. |
| The admin shows a few MB of data but the database folder takes over 100 MB | Normal. The admin shows the statistics themselves; MariaDB's folder also holds fixed-size files, mainly its 96 MB transaction log. Only the data part grows over time. |
- Designed for a home network. Do not expose it to the Internet without a reverse proxy with authentication and TLS.
- Ingestion requires the token (HTTP Basic or Bearer), compared in constant time; payload size is capped; every query is parameterised.
- The stats page is read-only. The admin uses a hashed password, CSRF tokens and
SameSite=Strictcookies. A strict Content-Security-Policy is sent; JavaScript libraries are bundled. - Complete the setup wizard right after installing: until then, anyone on your network could do it.
INGEST_ALLOWin.envcan restrict ingestion to given networks.- Radio reception and redistribution of AIS data depend on local regulations.
php tests/run.php # unit tests
DB_HOST=127.0.0.1 DB_NAME=test DB_USER=… DB_PASSWORD=… php tests/run.php # + integration testsStack: PHP 8.3 without framework, MariaDB, vanilla JavaScript with Chart.js and Leaflet bundled in public/assets/vendor. Scoping notes: docs/CADRAGE.md.
GPL-3.0-or-later, see LICENSE. Bundled: Chart.js (MIT), Leaflet (BSD-2-Clause). AISSeaStats is not affiliated with AIS-catcher. Not for navigation.







