Setting up media.path in Traccar: Saving and Serving Camera Photos
IMPORTANT
Configuring video for Traccar. The server tells the camera where to connect by reading web.url and the jt1078 / jimiphoto ports from your traccar.xml. If that's missing there's no live video and no alarm photos — and no error in the log either.
Reading order for this series: 1. how camera support works → 2. configure the server → 3. which camera to buy
If you already have a device with a compatible camera, enabling photo storage in Traccar is surprisingly simple: a single configuration key. What isn't so simple is everything that comes after — disk, permissions, and the proxy. Let's go through it piece by piece.
The real traccar.xml keys
There are only two keys related to multimedia. These:
<entry key='media.path'>/opt/traccar/media</entry>
<entry key='media.bufferSize'>33554432</entry>
media.path
This is the folder where the server stores audio, video, and photos. Its default value is ./media, relative to the server's working directory. Traccar automatically creates a subfolder per device using its unique identifier.
This key is also the switch. If media.path has a value, the servlet that serves the files gets registered; if not, it doesn't. Don't go looking for a media.enable key because it doesn't exist — it's a frequent mistake on the forum. Since the default value is already ./media, in practice the system is operational right out of the install; what's usually missing is pointing it to an absolute path with real space available.
media.bufferSize
The maximum size, in bytes, of a multimedia file a decoder can accumulate from a device. The default is 32 MB (33554432). Transfers that exceed that limit get discarded.
Two things to keep in mind:
- Only one buffer per connection is kept. Don't expect parallel transfers over the same socket.
- Cranking this value way up isn't free: it's memory the server reserves per active connection mid-transfer. If you're going to receive video clips from several vehicles at once, size your RAM accordingly.
For typical ADAS/DSM camera photos (100–500 KB) the default value is more than enough.
Where the files end up
The on-disk structure looks like this:
/opt/traccar/media/
├── 356938035643809/
│ ├── 1754400000000.jpg
│ └── 1754400600000.jpg
└── 862205059000000/
└── 1754401200000.h265
One folder per uniqueId (the IMEI or identifier you registered), and inside it files named by timestamp. On the corresponding position, Traccar adds an image, video, or audio attribute whose value is just the file name, not the path.
How they're accessed from outside
Traccar exposes the folder at:
GET /api/media/{uniqueId}/{fileName}
For example:
curl -u user:password \
https://your-server.com/api/media/356938035643809/1754400000000.jpg \
-o photo.jpg
This is not a public folder. In front of it there's a filter that requires a valid session and checks that the authenticated user has permission over that specific device. A user on your platform can't pull another customer's photos by guessing IMEIs.
Disk space: the part that bites
Nobody plans for this and then the server runs out of disk three weeks later. Do the math beforehand:
- A 300 KB event photo
- Times 10 events a day
- Times 50 vehicles
- Times 30 days
≈ 4.5 GB a month. With video the figure easily balloons into tens of GB.
Two practical consequences:
Mount
media.pathon a separate volume from the operating system. If it fills up, it shouldn't take down the database or the logs with it.Traccar's automatic cleanup of old positions does not delete the media files. If you purge the history, the JPGs are left orphaned on disk. You need your own cleanup job, for example:
# Delete media files older than 90 days find /opt/traccar/media -type f -mtime +90 -deleteTest it first without
-deleteand review the output before scheduling it in cron.
Permissions
The Traccar service needs write access to that folder. If you create it by hand as root, the service will fail silently on writes:
sudo mkdir -p /opt/traccar/media
sudo chown -R traccar:traccar /opt/traccar/media
Adjust the user to whatever your installation uses (in Docker it's usually different; there, what matters is mounting a persistent volume at the path you declare in media.path, or you'll lose your photos on every container recreation).
A note on the reverse proxy
If you have Nginx or Apache in front for HTTPS (and you should — see secure connection for Traccar), keep in mind that /api/media/ serves binary files that can be considerably heavier than a normal JSON API response.
If you see cut-off downloads or errors opening large video clips, check your proxy's response size limits and timeouts. In Nginx, the usual parameters to look at are proxy_read_timeout and response buffering.
And if you're also using JT1078 live video, there's a second path the proxy has to let through: /api/stream/, which serves the live.m3u8 playlist and the .ts segments. This is a classic failure: the video "won't start" when in fact the playlist loads fine but the segments 404 or get stuck in the proxy's buffering. I cover it in detail in JT1078 live video on Traccar.
NOTE
Traccar doesn't publish official proxy recommendations specific to media, so the above is general operating advice, not a configuration prescribed by the project. Start with the standard configuration and adjust only if you run into real failures.
If you have a JT808 MDVR, don't look for videos in media.path
A heads-up to save you hours of debugging: if your unit is an MDVR that speaks JT808/JT1078, you can configure media.path perfectly well and still not see a single video file there. That protocol doesn't use the media system: Traccar acts as a live relay and the recorded history stays on the vehicle's SD card.
What's normal on those servers is finding the folder with nothing but a device.jpg (the device's profile picture, which is a separate mechanism entirely). It isn't broken: it's the design. I explain it in live JT1078 video in Traccar. If you're choosing hardware, it's worth checking which cameras actually work with Traccar before you buy.
Configuring video for Traccar in traccar.xml
This is the part almost nobody documents: the 0x9101 command doesn't carry fixed values, it reads them from your configuration. If the XML is incomplete, Traccar still sends the command, logs no error at all, and the camera simply never shows up.
<entry key='web.url'>http://gps.yourdomain.com</entry>
<entry key='media.path'>/opt/traccar/media</entry>
<entry key='jt808.port'>5015</entry>
<entry key='jt1078.port'>5263</entry>
<entry key='jimiphoto.port'>5267</entry>
web.url — this is where the camera's target address comes from
When building videoStart, the JT808 encoder does URI.create(web.url).getHost() and writes that host into the frame. Translation: what the camera will try to resolve is the host in web.url.
Practical consequences:
- If
web.urlisn't defined, the video command never gets built. - If it points at
localhostor a private IP, the camera — sitting on a cellular network — will never reach it. It has to be a public host, resolvable from the vehicle's modem. - The
http://and the:8082you put there are irrelevant to the camera: only the hostname is used.
jt1078.port — the port that travels inside the command
The port Traccar writes into the 0x9101 is neither the web port nor the JT808 one: it's the jt1078 port, read from the configuration using the protocol prefix. Default is 5263.
And there's a detail here that breaks whole installations: a protocol only starts if its port is greater than zero. If you also declared protocols.enable, that key behaves as an allow-list — if jt1078 isn't in it, the port never listens and the camera ends up connecting to a closed port.
jimiphoto.port — alarm media upload
When an alarm with an attachment arrives, the JT808 decoder sends the camera an upload instruction shaped like this:
VIDEOUPLOAD,<host from web.url>,<jimiphoto.port>,<alarm label>,1-2-3,2
If web.url is empty or the jimiphoto port is zero, that function is skipped silently: nothing in the log, the attachment is simply never requested. It's the number-one cause of the classic "I get the alarm but never the photo".
<protocol>.address — watch the bind
There's a per-protocol .address suffix (jimiphoto.address, jt1078.address…) that defines which network interface that service listens on. If you don't declare it, it listens on all of them.
Setting it to 127.0.0.1 restricts the service to loopback, and then a camera out on the internet can't upload anything. It only makes sense if there's a proxy on the same machine in front of it. If in doubt, don't declare the key.
And a key that doesn't exist: media.server
Forums — and AI answers — pass around the idea of configuring something like this:
<!-- this does absolutely nothing -->
<entry key='media.server'>SERVER,2,gps.yourdomain.com,5263,0#</entry>
media.server does not exist in Traccar's code. That SERVER,2,...# is a device command for Jimi/Concox units to point them at a server, not a configuration key: harmless in the XML, but completely ignored. The address the camera receives comes from web.url, as explained above.
Firewall and Docker
All three ports have to be open inbound:
| Port | Service |
|---|---|
5015 | JT808 — telemetry and commands |
5263 | JT1078 — live video stream |
5267 | JimiPhoto — alarm media upload |
If the server runs in Docker you also have to publish them explicitly and mount media.path on a volume; otherwise you lose the files every time the container is recreated.
WARNING
Never publish or share your traccar.xml as-is: it carries your database credentials and, if you use notification webhooks, their private URLs. When asking for help, share only the relevant keys with placeholder values.
Verifying it works
Confirm your device uses a protocol with real media support — the list is short and I go through it in how the media system works.
Trigger a photo (via command, or by causing the event that generates one).
Check the folder:
ls -la /opt/traccar/media/YOUR_IMEI/If the file is on disk but you don't see it in the interface, the problem is user permissions on the device or the proxy — not the decoder.
If there's no file, check the server log: it's almost always that the protocol doesn't implement media, or that the transfer exceeded
media.bufferSize.