Traccar — Operations Workbook
GPS tracking server running on IONOS VPS (vps-i1).
Current state (2026-06-16):
| Item | Value |
|---|---|
| Image | traccar/traccar:6.13.3 (running) / 6.14.4 (pinned in compose — needs recreate) |
| MySQL image | mysql:8.4 |
| DB volume | 249 MB |
| Log volume | 11 MB |
| Backup | Daily 02:00 UTC → Wasabi S3 |
| Web UI | https://traccar.vps-i1.infra.zintegrowana.online |
| GPS port | 5027 TCP+UDP (public) |
Architecture
IONOS VPS (217.154.82.162)
├── Container: traccar image: traccar/traccar:6.14.4
│ ├── port 8082 web UI + REST API
│ ├── port 5027 tcp/udp GPS device input (Teltonika)
│ └── mounts:
│ ├── /root/traccar/traccar.xml → /opt/traccar/conf/traccar.xml (bind, :ro)
│ ├── traccar_traccar-data → /opt/traccar/data (volume)
│ └── traccar_traccar-logs → /opt/traccar/logs (volume)
│
├── Container: traccar-db image: mysql:8.0
│ └── traccar_traccar-db → /var/lib/mysql (~210 MB, grows with position history)
│
└── Network: traccar_traccar-net (also attached to monitoring-caddy-1 for reverse proxy)URLs:
- Web UI:
https://traccar.vps-i1.infra.zintegrowana.online - Internal:
http://217.154.82.162:8082
Compose file on server: /root/traccar/docker-compose.yml
Compose file in repo: services/traccar/docker-compose.yml (canonical — keep in sync)
Config Management
Files
| File | Location on server | In repo? | Contains secrets? |
|---|---|---|---|
docker-compose.yml | /root/traccar/docker-compose.yml | ✅ services/traccar/ | No |
traccar.xml | /root/traccar/traccar.xml | ✅ services/traccar/traccar.xml (sanitized) | Yes — DB password hardcoded |
.env | /root/traccar/.env | ❌ .env.example only | Yes |
traccar.xml — password injection
Traccar XML does not support environment variable substitution. The DB password must be
written directly into traccar.xml. Never commit the live file with the real password.
The file in repo (services/traccar/traccar.xml) uses the placeholder:
TRACCAR_DB_PASSWORD_PLACEHOLDERWhen provisioning a new server — use the generate-config script:
bash /opt/p24-infra/services/traccar/scripts/generate-config.shThis reads MYSQL_PASSWORD from /root/traccar/.env and writes the live traccar.xml.
Updating config
# Edit the repo template (non-secret changes only)
nano /opt/p24-infra/services/traccar/traccar.xml
# Regenerate live file
bash /opt/p24-infra/services/traccar/scripts/generate-config.sh
# Restart to apply (config is :ro mount)
cd /root/traccar && docker compose restart traccarBackup
What to back up
| Data | Method | Frequency |
|---|---|---|
| MySQL database | mysqldump → Wasabi S3 | Daily (automated) |
traccar.xml (sanitized) | In repo | On every change |
.env | Bitwarden / manual secure copy | On every change |
traccar-data volume | Not needed (currently empty — stores media attachments) | — |
traccar-logs volume | Not backed up (logs only) | — |
Manual backup
# SSH to server
ssh -i C:\Users\konar\.ssh\id_ed25519 root@217.154.82.162
# Dump MySQL to file
docker exec traccar-db mysqldump \
-utraccar -p"$(grep MYSQL_PASSWORD /root/traccar/.env | cut -d= -f2)" \
--single-transaction --quick \
traccar > /root/traccar/backup_$(date +%Y%m%d_%H%M%S).sql
# Verify
ls -lh /root/traccar/backup_*.sqlAutomated daily backup
Script: services/traccar/scripts/backup.py (deployed to /root/traccar/scripts/backup.py)
Cron: daily at 02:00 UTC
Upload to: s3://ecotrans-monitoring/backups/traccar/traccar-YYYY-MM-DD.sql.gz
Retention: 30 days (auto-purge on each run)
# Run manually to test
python3 /root/traccar/scripts/backup.py
# Check logs
tail -50 /var/log/traccar-backup.logRestore
Restore database from dump
# 1. Copy dump to server or download from Wasabi
# 2. Stop traccar (keep DB running)
cd /root/traccar && docker compose stop traccar
# 3. Restore
docker exec -i traccar-db mysql \
-utraccar -p"$(grep MYSQL_PASSWORD /root/traccar/.env | cut -d= -f2)" \
traccar < /path/to/backup.sql
# 4. Start traccar
docker compose start traccar
# 5. Verify
docker logs traccar --tail=30Full restore from scratch (new volume)
# 1. Stop and remove containers + volumes
cd /root/traccar
docker compose down -v # WARNING: destroys all data — only if you have a dump
# 2. Start fresh DB
docker compose up -d db
# 3. Wait for MySQL to initialize (~10s), then restore
sleep 15
docker exec -i traccar-db mysql \
-utraccar -p"$(grep MYSQL_PASSWORD /root/traccar/.env | cut -d= -f2)" \
traccar < /path/to/backup.sql
# 4. Start traccar
docker compose up -d traccarFresh Install (new server, no data)
Use this when setting up Traccar on a brand-new server with no existing data. For migrating an existing installation see Migration to a Larger Server.
Prerequisites
- Docker + Docker Compose installed
/opt/p24-infracloned (git clone https://github.com/radieu/p24-infra /opt/p24-infra)- SSH key access from local workstation
Step 1 — Create runtime directory and .env
mkdir -p /root/traccar/scripts
cat > /root/traccar/.env << 'EOF'
MYSQL_ROOT_PASSWORD=<strong-random-password>
MYSQL_DATABASE=traccar
MYSQL_USER=traccar
MYSQL_PASSWORD=<strong-random-password>
EOF
chmod 600 /root/traccar/.envStep 2 — Generate traccar.xml (injects DB password)
cp /opt/p24-infra/services/traccar/scripts/generate-config.sh /root/traccar/scripts/
bash /root/traccar/scripts/generate-config.sh
# Verify: grep -v password /root/traccar/traccar.xmlStep 3 — Copy compose file and web assets
cp /opt/p24-infra/services/traccar/docker-compose.yml /root/traccar/docker-compose.yml
mkdir -p /root/traccar/web
cp /opt/p24-infra/services/traccar/web/* /root/traccar/web/Step 4 — Start stack
cd /root/traccar
docker compose up -d
docker compose ps # both traccar and traccar-db should be Up
docker logs traccar --tail=30Traccar takes ~20s to initialize DB schema on first start. Watch for:
INFO: Started ServerConnector@... {HTTP/1.1}Step 5 — Connect to monitoring Caddy network
# Caddy and Traccar must share a network for reverse proxy
docker network connect monitoring_monitoring-net traccar 2>/dev/null || trueAdd Caddyfile block in /opt/p24-infra/monitoring/Caddyfile (then reload Caddy):
traccar.vps-NEW.infra.zintegrowana.online {
@nextjs_exploit {
header Next-Action *
}
respond @nextjs_exploit 403
reverse_proxy traccar:8082
encode gzip
}Step 6 — DNS
python3 /opt/p24-infra/scripts/dns-manager.py upsert \
"traccar.vps-NEW.infra.zintegrowana.online" NEW_SERVER_IPStep 7 — Deploy backup script
cp /opt/p24-infra/services/traccar/scripts/backup.py /root/traccar/scripts/backup.py
# Add cron (daily 02:00 UTC)
(crontab -l 2>/dev/null; echo "0 2 * * * python3 /root/traccar/scripts/backup.py >> /var/log/traccar-backup.log 2>&1") | crontab -Step 8 — Create admin user and add devices
Open https://traccar.vps-NEW.infra.zintegrowana.online → register first user (becomes admin).
Then: Settings → Devices → Add device for each Teltonika tracker (see Device Management).
Device Management
How Teltonika devices connect
Teltonika FMB/FMC/FMT series devices connect via TCP on port 5027 using the Traccar Teltonika protocol. The device sends its IMEI during the handshake — Traccar matches it to a registered device record.
Device registration flow:
- SIM card inside device dials
217.154.82.162:5027 - Traccar receives IMEI — looks up
devices.uniqueid - If found: position data is stored in
positions - If NOT found: data is discarded silently (no error to device)
Adding a new device
- Open Traccar web UI → Urządzenia → ⊕ Dodaj
- Fill in:
- Nazwa: vehicle plate or identifier (e.g.
WE 12345) - Identyfikator: 15-digit IMEI from device label (e.g.
861327085909831)
- Nazwa: vehicle plate or identifier (e.g.
- Save — device is active immediately
- Verify connection within ~5 min:
traccar-logs --days 1 --incoming --imei 861327085909831Checking device status
# Which devices connected today?
traccar-logs --days 1 --incoming | grep -oP '\d{9,15}(?=\])' | sort -u
# Last packet from a specific device
traccar-logs --days 7 --incoming | grep "861327085909831" | tail -1
# How many packets in last 7 days (active = >10/day expected)
traccar-logs --days 7 --incoming | grep "861327" | wc -lIn the web UI: Raporty → Zdarzenia (date filter works here) shows connect/disconnect history. Raporty → Logi is real-time only — no historical data.
Teltonika device configuration (FMB/FMC)
Configure via Teltonika Configurator (USB or GPRS):
| Setting | Value |
|---|---|
| Server IP | 217.154.82.162 |
| Server port | 5027 |
| Protocol | TCP |
| APN | SIM card APN (e.g. internet for T-Mobile PL) |
| Send period | 30–60s (moving), 300s (stationary) |
Maintenance
Routine maintenance schedule
| Task | Frequency | Command / Action |
|---|---|---|
| Check backup status | Weekly | tail -20 /var/log/traccar-backup.log |
| Check DB size | Monthly | See DB size commands below |
| Prune old positions | Quarterly or when DB >500 MB | See pruning below |
| Rotate DB password | Annually or on staff change | See password rotation below |
| Update Traccar image | On CVE/major release | See Upgrade |
Check backup status
# Last 5 backup runs
tail -50 /var/log/traccar-backup.log
# Latest backup in Wasabi
python3 - << 'EOF'
import boto3, os
s3 = boto3.client('s3',
endpoint_url='https://s3.eu-central-2.wasabisys.com',
aws_access_key_id=os.environ.get('WASABI_ACCESS_KEY'),
aws_secret_access_key=os.environ.get('WASABI_SECRET_KEY'))
objs = s3.list_objects_v2(Bucket='p24-infra', Prefix='backups/traccar/')
for o in sorted(objs.get('Contents',[]), key=lambda x: x['LastModified'])[-3:]:
print(o['Key'], round(o['Size']/1024/1024,1), 'MB', o['LastModified'].date())
EOFCheck DB size
PASS=$(grep MYSQL_PASSWORD /root/traccar/.env | cut -d= -f2)
# Table sizes
docker exec traccar-db mysql -utraccar -p"$PASS" -e "
SELECT table_name,
ROUND((data_length + index_length)/1024/1024, 1) AS mb
FROM information_schema.tables
WHERE table_schema = 'traccar'
ORDER BY (data_length + index_length) DESC;
" 2>/dev/null
# Row counts
docker exec traccar-db mysql -utraccar -p"$PASS" traccar -e "
SELECT
(SELECT COUNT(*) FROM devices) AS devices,
(SELECT COUNT(*) FROM positions) AS positions,
(SELECT COUNT(*) FROM events) AS events;
" 2>/dev/nullPrune old positions (when DB grows >500 MB)
Always back up first.
PASS=$(grep MYSQL_PASSWORD /root/traccar/.env | cut -d= -f2)
KEEP_DAYS=180 # keep last 6 months
# Dry run — count rows to delete
docker exec traccar-db mysql -utraccar -p"$PASS" traccar -e "
SELECT COUNT(*) AS rows_to_delete
FROM positions
WHERE servertime < DATE_SUB(NOW(), INTERVAL ${KEEP_DAYS} DAY);
" 2>/dev/null
# Execute delete (run during low-traffic window)
docker exec traccar-db mysql -utraccar -p"$PASS" traccar -e "
DELETE FROM positions
WHERE servertime < DATE_SUB(NOW(), INTERVAL ${KEEP_DAYS} DAY);
" 2>/dev/null
# Reclaim disk space
docker exec traccar-db mysql -utraccar -p"$PASS" traccar -e "OPTIMIZE TABLE positions;" 2>/dev/nullRotate DB password
- Generate new password:
openssl rand -base64 24 - Stop Traccar:
cd /root/traccar && docker compose stop traccar - Update MySQL:
PASS=$(grep MYSQL_PASSWORD /root/traccar/.env | cut -d= -f2)
NEWPASS="<new-password>"
docker exec traccar-db mysql -uroot -p"$(grep MYSQL_ROOT_PASSWORD /root/traccar/.env | cut -d= -f2)" \
-e "ALTER USER 'traccar'@'%' IDENTIFIED BY '$NEWPASS'; FLUSH PRIVILEGES;" 2>/dev/null- Update
.env:sed -i "s/MYSQL_PASSWORD=.*/MYSQL_PASSWORD=$NEWPASS/" /root/traccar/.env - Regenerate
traccar.xml:bash /root/traccar/scripts/generate-config.sh - Start Traccar:
docker compose start traccar - Verify:
docker logs traccar --tail=20
Health check (one-stop diagnostic)
echo "=== Containers ===" && \
docker ps --filter name=traccar --format "{{.Names}}: {{.Status}}"
echo "=== Web UI ===" && \
curl -s -o /dev/null -w "HTTP %{http_code}\n" https://traccar.vps-i1.infra.zintegrowana.online
echo "=== GPS port (Teltonika) ===" && \
timeout 3 bash -c "echo > /dev/tcp/217.154.82.162/5027" 2>/dev/null && \
echo "port 5027 open" || echo "port 5027 unreachable"
echo "=== Last backup ===" && \
tail -3 /var/log/traccar-backup.log
echo "=== DB size ===" && \
PASS=$(grep MYSQL_PASSWORD /root/traccar/.env | cut -d= -f2) && \
docker exec traccar-db mysql -utraccar -p"$PASS" -e \
"SELECT ROUND(SUM(data_length+index_length)/1024/1024,1) AS mb FROM information_schema.tables WHERE table_schema='traccar';" 2>/dev/null
echo "=== Device activity (today) ===" && \
traccar-logs --days 1 --incoming 2>/dev/null | wc -l | xargs -I{} echo "{} incoming packets"Migration to a Larger Server
Full migration checklist (e.g. current IONOS → OVH Server F).
Step 1 — Prepare new server
# Install Docker on new server
# Clone repo
git clone https://github.com/radieu/p24-infra /opt/p24-infra
mkdir -p /root/traccar/scriptsStep 2 — Create .env on new server
# Copy .env from old server or recreate from Bitwarden
scp -i ~/.ssh/id_ed25519 root@217.154.82.162:/root/traccar/.env /root/traccar/.envStep 3 — Generate traccar.xml on new server
cp /opt/p24-infra/services/traccar/scripts/generate-config.sh /root/traccar/scripts/
bash /root/traccar/scripts/generate-config.shStep 4 — Take final backup on old server
# On OLD server
cd /root/traccar
docker compose stop traccar # stop writes, keep DB up
docker exec traccar-db mysqldump \
-utraccar -p"$(grep MYSQL_PASSWORD .env | cut -d= -f2)" \
--single-transaction --quick --routines \
traccar > /root/traccar/migration_$(date +%Y%m%d).sqlStep 5 — Transfer dump to new server
# From local machine
scp -i C:\Users\konar\.ssh\id_ed25519 \
root@217.154.82.162:/root/traccar/migration_*.sql \
root@NEW_SERVER_IP:/root/traccar/Step 6 — Start and restore on new server
# On NEW server
cd /root/traccar
docker compose up -d db
sleep 15
docker exec -i traccar-db mysql \
-utraccar -p"$(grep MYSQL_PASSWORD .env | cut -d= -f2)" \
traccar < /root/traccar/migration_*.sql
docker compose up -d traccarStep 7 — Verify on new server
# Check containers are healthy
docker compose ps
docker logs traccar --tail=30
# Check device count in DB
docker exec traccar-db mysql -utraccar \
-p"$(grep MYSQL_PASSWORD /root/traccar/.env | cut -d= -f2)" \
traccar -e "SELECT COUNT(*) as devices FROM devices; SELECT COUNT(*) as positions FROM positions;"
# Test web UI
curl -s -o /dev/null -w "%{http_code}" http://localhost:8082Step 8 — Update DNS
# Point traccar.vps-i1 (or new label) to new server IP
python3 /opt/p24-infra/scripts/dns-manager.py upsert \
"traccar.vps-NEW.infra.zintegrowana.online" NEW_SERVER_IPStep 9 — Update Caddyfile on new server
Add reverse proxy block for traccar.vps-NEW.infra.zintegrowana.online in the monitoring Caddyfile.
Step 10 — Decommission old server
# On OLD server — stop Traccar only (not the whole monitoring stack)
cd /root/traccar && docker compose downUpgrade Traccar Version
# 1. Backup first (always)
python3 /root/traccar/scripts/backup.py
# 2. Pull new image
cd /root/traccar
docker compose pull traccar
# 3. Recreate container
docker compose up -d traccar
# 4. Check logs for migration messages
docker logs traccar --tail=50 -f
# 5. Update version pin in repo
# Edit services/traccar/docker-compose.yml: traccar/traccar:X.Y.Z → new version
# Commit and pushNote: Traccar auto-runs DB schema migrations on startup. Always back up before upgrading.
Monitoring
Prometheus alerts active (since 2026-05-13):
TraccarDown— critical: container not seen by cAdvisor for >2minTraccarHighRestarts— warning: >2 restarts in 1 hourblackbox— HTTP probe every 30s onhttps://traccar.vps-i1.infra.zintegrowana.online
Grafana: no dedicated Traccar dashboard yet. Container metrics visible in the cAdvisor panels.
Raw Device Communication Logs
Jak działa logowanie w Traccar 6.x
Traccar domyślnie zapisuje całą komunikację z urządzeniami do pliku logów. Nie ma tabeli tc_logs w bazie danych — to celowy design: logi są plikowe, a web UI “Logi” to real-time stream przez WebSocket (dane widać tylko gdy strona jest otwarta w momencie odbioru pakietu).
| Mechanizm | Opis |
|---|---|
| Plik logów (domyślny) | Codziennie rotowany, pełna historia od uruchomienia |
| Web UI → Raporty → Logi | Real-time stream — bez historii, bez filtra dat |
logger.console=true | Dodatkowy output na stdout (docker logs) — dodany 2026-06-16 |
Lokalizacja plików
Host: /var/lib/docker/volumes/traccar_traccar-logs/_data/
Kontener: /opt/traccar/logs/| Plik | Zawartość |
|---|---|
tracker-server.log | Bieżący dzień |
tracker-server.log.YYYYMMDD | Archiwum — dzienna rotacja od 2026-04-30 |
Format wpisu (raw bytes)
2026-06-16 09:47:50 INFO: [Udcf9080e: teltonika < 80.249.102.72] 0054cafe010f...
2026-06-16 09:47:50 INFO: [Udcf9080e: teltonika > 80.249.102.72] 00050000010f01| Pole | Znaczenie |
|---|---|
Udcf9080e | ID sesji Netty (hex) |
teltonika | Protokół GPS |
< | Dane przychodzące od urządzenia |
> | Odpowiedź serwera (ACK) |
80.249.102.72 | IP SIM karty urządzenia |
0054cafe... | Surowe bajty pakietu (hex dump) |
Skrypt traccar-logs (zainstalowany na vps-i1)
# Ostatnie 7 dni — wszystkie urządzenia (dane przychodzące + ACK)
traccar-logs --days 7
# Tylko dane od urządzeń (bez ACK)
traccar-logs --days 7 --incoming
# Tylko ACK serwera
traccar-logs --days 1 --outgoing
# Filtrowanie po IMEI (lub prefiksie)
traccar-logs --days 7 --imei 861327085909831
# Live tail w czasie rzeczywistym
tail -f /var/lib/docker/volumes/traccar_traccar-logs/_data/tracker-server.log \
| grep "teltonika"Plik skryptu: /usr/local/bin/traccar-logs
Web UI → Raporty → Logi
- Pokazuje raw bytes tylko w czasie rzeczywistym — dane przepadają gdy strona jest zamknięta
- Brak filtra dat — to ograniczenie architektury (WebSocket push, nie DB query)
- Dane historyczne dostępne wyłącznie przez plik logów lub skrypt
traccar-logs - Dla historii połączeń/rozłączeń: Raporty → Zdarzenia (korzysta z
tc_eventsw DB, ma filtr dat i działa poprawnie)
Diagnostyka połączeń urządzenia
# Czy urządzenie łączy się dziś?
traccar-logs --days 1 --incoming --imei 861327085909831
# Ile pakietów wysłało urządzenie w ciągu 7 dni?
traccar-logs --days 7 --incoming | grep "861327" | wc -l
# Ostatni pakiet z urządzenia
traccar-logs --days 7 --incoming | grep "861327" | tail -1Security
Port exposure (current state 2026-06-16)
| Port | Protocol | Exposure | Notes |
|---|---|---|---|
| 5027 | TCP+UDP | Public (0.0.0.0) | Required — GPS devices connect here |
| 8082 | HTTP | Localhost only | Proxied by Caddy via HTTPS |
All other Traccar internal protocol ports (5001-5263) are inside Docker network traccar-net only — not mapped to host.
Caddy WAF — Next-Action header block
Active exploit attempts were found in logs (2026-06-11, 2026-06-14, 2026-06-16) where attackers sent Next.js Server Action payloads via the OsmAnd HTTP path on port 8082. Caddy blocks these at the reverse proxy layer:
@nextjs_exploit {
header Next-Action *
}
respond @nextjs_exploit 403Requests with Next-Action header return 403 Forbidden from Caddy before reaching Traccar.
Attacker IPs seen: 213.246.36.120, 80.249.102.72
OsmAnd protocol
Traccar on port 8082 accepts HTTP requests and processes them as OsmAnd protocol if they match the format. The attack payload was not executed (Traccar is Java, not Node.js) but was logged. With the Caddy WAF rule in place, these requests no longer reach Traccar.
Known Issues / TODOs
| # | Issue | Priority | Notes |
|---|---|---|---|
| 1 | Compose file lives in /root/traccar/ not /opt/p24-infra/services/traccar/ | Low | Runtime dir diverged from repo — keep in sync manually |
| 2 | No dedicated Grafana dashboard for Traccar | Low | Container metrics visible in cAdvisor panels |
| 3 | Running image 6.13.3 differs from compose pin 6.14.4 | Medium | cd /root/traccar && docker compose pull traccar && docker compose up -d traccar |