Traccar — Operations Workbook

GPS tracking server running on IONOS VPS (vps-i1).

Current state (2026-06-16):

ItemValue
Imagetraccar/traccar:6.13.3 (running) / 6.14.4 (pinned in compose — needs recreate)
MySQL imagemysql:8.4
DB volume249 MB
Log volume11 MB
BackupDaily 02:00 UTC → Wasabi S3
Web UIhttps://traccar.vps-i1.infra.zintegrowana.online
GPS port5027 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

FileLocation on serverIn repo?Contains secrets?
docker-compose.yml/root/traccar/docker-compose.ymlservices/traccar/No
traccar.xml/root/traccar/traccar.xmlservices/traccar/traccar.xml (sanitized)Yes — DB password hardcoded
.env/root/traccar/.env.env.example onlyYes

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_PLACEHOLDER

When provisioning a new server — use the generate-config script:

bash /opt/p24-infra/services/traccar/scripts/generate-config.sh

This 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 traccar

Backup

What to back up

DataMethodFrequency
MySQL databasemysqldump → Wasabi S3Daily (automated)
traccar.xml (sanitized)In repoOn every change
.envBitwarden / manual secure copyOn every change
traccar-data volumeNot needed (currently empty — stores media attachments)
traccar-logs volumeNot 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_*.sql

Automated 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.log

Restore

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=30

Full 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 traccar

Fresh 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-infra cloned (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/.env

Step 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.xml

Step 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=30

Traccar 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 || true

Add 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_IP

Step 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:

  1. SIM card inside device dials 217.154.82.162:5027
  2. Traccar receives IMEI — looks up devices.uniqueid
  3. If found: position data is stored in positions
  4. If NOT found: data is discarded silently (no error to device)

Adding a new device

  1. Open Traccar web UI → Urządzenia⊕ Dodaj
  2. Fill in:
    • Nazwa: vehicle plate or identifier (e.g. WE 12345)
    • Identyfikator: 15-digit IMEI from device label (e.g. 861327085909831)
  3. Save — device is active immediately
  4. Verify connection within ~5 min:
traccar-logs --days 1 --incoming --imei 861327085909831

Checking 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 -l

In 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):

SettingValue
Server IP217.154.82.162
Server port5027
ProtocolTCP
APNSIM card APN (e.g. internet for T-Mobile PL)
Send period30–60s (moving), 300s (stationary)

Maintenance

Routine maintenance schedule

TaskFrequencyCommand / Action
Check backup statusWeeklytail -20 /var/log/traccar-backup.log
Check DB sizeMonthlySee DB size commands below
Prune old positionsQuarterly or when DB >500 MBSee pruning below
Rotate DB passwordAnnually or on staff changeSee password rotation below
Update Traccar imageOn CVE/major releaseSee 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())
EOF

Check 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/null

Prune 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/null

Rotate DB password

  1. Generate new password: openssl rand -base64 24
  2. Stop Traccar: cd /root/traccar && docker compose stop traccar
  3. 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
  1. Update .env: sed -i "s/MYSQL_PASSWORD=.*/MYSQL_PASSWORD=$NEWPASS/" /root/traccar/.env
  2. Regenerate traccar.xml: bash /root/traccar/scripts/generate-config.sh
  3. Start Traccar: docker compose start traccar
  4. 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/scripts

Step 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/.env

Step 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.sh

Step 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).sql

Step 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 traccar

Step 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:8082

Step 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_IP

Step 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 down

Upgrade 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 push

Note: 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 >2min
  • TraccarHighRestarts — warning: >2 restarts in 1 hour
  • blackbox — HTTP probe every 30s on https://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).

MechanizmOpis
Plik logów (domyślny)Codziennie rotowany, pełna historia od uruchomienia
Web UI → Raporty → LogiReal-time stream — bez historii, bez filtra dat
logger.console=trueDodatkowy 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/
PlikZawartość
tracker-server.logBieżący dzień
tracker-server.log.YYYYMMDDArchiwum — 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
PoleZnaczenie
Udcf9080eID sesji Netty (hex)
teltonikaProtokół GPS
<Dane przychodzące od urządzenia
>Odpowiedź serwera (ACK)
80.249.102.72IP 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_events w 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 -1

Security

Port exposure (current state 2026-06-16)

PortProtocolExposureNotes
5027TCP+UDPPublic (0.0.0.0)Required — GPS devices connect here
8082HTTPLocalhost onlyProxied 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 403

Requests 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

#IssuePriorityNotes
1Compose file lives in /root/traccar/ not /opt/p24-infra/services/traccar/LowRuntime dir diverged from repo — keep in sync manually
2No dedicated Grafana dashboard for TraccarLowContainer metrics visible in cAdvisor panels
3Running image 6.13.3 differs from compose pin 6.14.4Mediumcd /root/traccar && docker compose pull traccar && docker compose up -d traccar