<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Telematics Lab</title>
    <description>The latest articles on DEV Community by Telematics Lab (@telematicslab).</description>
    <link>https://dev.to/telematicslab</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4175083%2Fd92ce107-8fe5-4a4e-a772-d275be7b0302.png</url>
      <title>DEV Community: Telematics Lab</title>
      <link>https://dev.to/telematicslab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/telematicslab"/>
    <language>en</language>
    <item>
      <title>Why Traccar's WebSocket needs a proxy, and how to set one up with nginx</title>
      <dc:creator>Telematics Lab</dc:creator>
      <pubDate>Sat, 10 Oct 2026 11:48:57 +0000</pubDate>
      <link>https://dev.to/telematicslab/why-traccars-websocket-needs-a-proxy-and-how-to-set-one-up-with-nginx-1in0</link>
      <guid>https://dev.to/telematicslab/why-traccars-websocket-needs-a-proxy-and-how-to-set-one-up-with-nginx-1in0</guid>
      <description>&lt;p&gt;You have Traccar running, the REST API answers, and now you want a custom web map that moves vehicles live. You open a WebSocket to &lt;code&gt;/api/socket&lt;/code&gt;, and it fails. Or it connects from Traccar's own UI but never from your page.&lt;/p&gt;

&lt;p&gt;This isn't a bug in your code. It comes from two design choices in Traccar, and once you see them, the fix is a short nginx config.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two problems
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The WebSocket only accepts a session cookie.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Traccar's REST API supports several ways to authenticate: a session cookie, a standard HTTP &lt;code&gt;Authorization&lt;/code&gt; header, a bearer token. The WebSocket doesn't. Traccar's API documentation says it plainly: "Session cookie is the only authorization option for the WebSocket connection."&lt;/p&gt;

&lt;p&gt;In the browser you can't add headers to a WebSocket anyway: the &lt;code&gt;WebSocket&lt;/code&gt; constructor takes a URL and an optional list of subprotocols, nothing else. So the only way to authenticate is to have the browser send Traccar's &lt;code&gt;JSESSIONID&lt;/code&gt; cookie with the handshake, and it only does that automatically for a request to the same site that set the cookie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Traccar sends no CORS headers by default.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your map lives on &lt;code&gt;http://localhost:3000&lt;/code&gt; and Traccar on &lt;code&gt;http://localhost:8082&lt;/code&gt;, those are two different origins. Your &lt;code&gt;fetch('/api/session')&lt;/code&gt; to log in becomes a cross-origin request, the browser blocks the response (no &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;), and even if you opened CORS up, you'd then be fighting the browser's rules for cookies on cross-origin requests.&lt;/p&gt;

&lt;p&gt;You can try to solve both on the Traccar side. Or you can make them not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: one origin
&lt;/h2&gt;

&lt;p&gt;Put a reverse proxy in front of everything. The proxy serves your frontend files &lt;strong&gt;and&lt;/strong&gt; forwards &lt;code&gt;/api/...&lt;/code&gt; to Traccar. From the browser's point of view there is only one server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  browser  ── http://localhost:8080 ──►  nginx
                                          │  /            → your HTML/JS (static files)
                                          │  /api/*       → Traccar REST API
                                          │  /api/socket  → Traccar WebSocket (Upgrade forwarded)
                                          ▼
                                       traccar:8082
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;POST /api/session&lt;/code&gt; sets the cookie on &lt;code&gt;localhost:8080&lt;/code&gt;, the WebSocket to &lt;code&gt;ws://localhost:8080/api/socket&lt;/code&gt; goes to the same origin, the browser attaches the cookie by itself, and CORS never comes up. No change to Traccar's configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The nginx config
&lt;/h2&gt;

&lt;p&gt;Here is the complete &lt;code&gt;default.conf&lt;/code&gt;, unchanged. It was tested with &lt;code&gt;traccar/traccar:6.15.3-alpine&lt;/code&gt; and &lt;code&gt;nginx:1.27-alpine&lt;/code&gt; in Docker Compose (Docker Desktop on Windows, Chrome).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# nginx serves the frontend AND proxies the Traccar API on the same origin.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Why: Traccar's WebSocket (/api/socket) only accepts a session cookie, and&lt;/span&gt;
&lt;span class="c1"&gt;# Traccar does not send CORS headers by default. If the page and the API share&lt;/span&gt;
&lt;span class="c1"&gt;# one origin (http://localhost:8080), the browser sends the cookie&lt;/span&gt;
&lt;span class="c1"&gt;# automatically and CORS never comes up. No Traccar config changes needed.&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;root&lt;/span&gt;  &lt;span class="n"&gt;/usr/share/nginx/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Live updates: WebSocket upgrade must be forwarded explicitly.&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/api/socket&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://traccar:8082&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;"upgrade"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;# keep idle sockets open&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Everything else under /api -&amp;gt; Traccar REST API.&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://traccar:8082&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Always serve fresh files while you edit the frontend.&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"no-store"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each block does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;location = /api/socket&lt;/code&gt;&lt;/strong&gt;: the &lt;code&gt;=&lt;/code&gt; makes this an exact match, and nginx always checks exact matches before prefix matches like &lt;code&gt;/api/&lt;/code&gt;. So the socket gets its own block with the upgrade settings, and the rest of the API doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;proxy_http_version 1.1&lt;/code&gt; + &lt;code&gt;Upgrade&lt;/code&gt; + &lt;code&gt;Connection "upgrade"&lt;/code&gt;&lt;/strong&gt;: a WebSocket starts as an HTTP/1.1 request asking to "upgrade" the connection. &lt;code&gt;Upgrade&lt;/code&gt; and &lt;code&gt;Connection&lt;/code&gt; are hop-by-hop headers, so nginx doesn't pass them on unless you set them explicitly. Without these three lines, Traccar receives a plain GET and the handshake fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;proxy_read_timeout 1h&lt;/code&gt;&lt;/strong&gt;: nginx's default is 60 seconds. If no message passes for a minute (a parked vehicle, a quiet night), nginx closes the socket. A long timeout keeps idle connections open; your client should still reconnect when it does close (see below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;location /api/&lt;/code&gt;&lt;/strong&gt;: everything else (&lt;code&gt;/api/session&lt;/code&gt;, &lt;code&gt;/api/devices&lt;/code&gt;, &lt;code&gt;/api/positions&lt;/code&gt;, …) goes to Traccar as normal HTTP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;location /&lt;/code&gt;&lt;/strong&gt;: your static frontend. &lt;code&gt;no-store&lt;/code&gt; is only there for development so you always see your latest edit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Docker Compose, &lt;code&gt;traccar&lt;/code&gt; is the service name, so &lt;code&gt;http://traccar:8082&lt;/code&gt; resolves inside the Compose network. The web service just mounts the frontend and this config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:1.27-alpine&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traccar&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${WEB_PORT:-8080}:80"&lt;/span&gt;             &lt;span class="c1"&gt;# open http://localhost:8080&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./frontend:/usr/share/nginx/html:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outside Docker, replace &lt;code&gt;traccar:8082&lt;/code&gt; with wherever Traccar listens, for example &lt;code&gt;127.0.0.1:8082&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the browser connects
&lt;/h2&gt;

&lt;p&gt;With the proxy in place, the frontend uses &lt;strong&gt;relative URLs only&lt;/strong&gt;. That's the whole trick: nothing in the JavaScript knows Traccar's real address.&lt;/p&gt;

&lt;p&gt;First, open a session. Traccar's login takes form-encoded email and password and answers with the session cookie:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then connect the WebSocket on the same host. The cookie goes along automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;retryDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;connectSocket&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ws&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/socket`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onopen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;setConn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;retryDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;onDevices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;onPositions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// data.events: server-side events (geofence, overspeed…), not used here.&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onclose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setConn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Reconnect with backoff; re-login if Traccar was restarted (session lost).&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitForServer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ensureSession&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;askForLogin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;connectSocket&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;retryDelay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;retryDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;retryDelay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;setConn&lt;/code&gt; only updates an online/offline label; &lt;code&gt;waitForServer&lt;/code&gt; and &lt;code&gt;ensureSession&lt;/code&gt; are explained in the pitfalls below.&lt;/p&gt;

&lt;p&gt;Each message is a JSON object with a key per type: &lt;code&gt;devices&lt;/code&gt;, &lt;code&gt;positions&lt;/code&gt; or &lt;code&gt;events&lt;/code&gt;. A key is simply missing when there's nothing of that type. &lt;code&gt;onPositions&lt;/code&gt; is where you move your Leaflet markers.&lt;/p&gt;

&lt;p&gt;The order matters: load the current state over REST first (&lt;code&gt;GET /api/devices&lt;/code&gt;, &lt;code&gt;GET /api/positions&lt;/code&gt; returns the latest position of each device), draw it, &lt;em&gt;then&lt;/em&gt; open the socket for changes. Otherwise the map stays empty until each vehicle sends its next position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hard-coding Traccar's address in the frontend.&lt;/strong&gt; &lt;code&gt;new WebSocket('ws://localhost:8082/api/socket')&lt;/code&gt; skips the proxy, so you're cross-origin again and the cookie set on &lt;code&gt;:8080&lt;/code&gt; isn't sent. Keep every URL relative or built from &lt;code&gt;location.host&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ws://&lt;/code&gt; on an HTTPS page.&lt;/strong&gt; Browsers block an insecure WebSocket from a secure page. Build the scheme from &lt;code&gt;location.protocol&lt;/code&gt;, as above, and terminate TLS at nginx: the proxy speaks plain HTTP to Traccar on the internal network, the browser gets &lt;code&gt;wss://&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting the upgrade headers.&lt;/strong&gt; The REST API works, the socket doesn't, and nginx's log shows a normal request to &lt;code&gt;/api/socket&lt;/code&gt;. That's the missing &lt;code&gt;proxy_http_version 1.1&lt;/code&gt; / &lt;code&gt;Upgrade&lt;/code&gt; / &lt;code&gt;Connection&lt;/code&gt; trio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 60-second disconnect.&lt;/strong&gt; Everything works, then the status flips to offline once a minute when nothing is moving. That's nginx's default &lt;code&gt;proxy_read_timeout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sessions don't survive a Traccar restart.&lt;/strong&gt; When Traccar restarts, the socket closes and the old cookie is no longer valid. Reconnecting the WebSocket alone fails forever. Check the session first and log in again, which is what &lt;code&gt;ensureSession()&lt;/code&gt; does in the &lt;code&gt;onclose&lt;/code&gt; handler above. Note that &lt;code&gt;GET /api/session&lt;/code&gt; answers 404 when there's no session yet; treat that as "not logged in", not as an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traccar takes a while to boot.&lt;/strong&gt; Right after &lt;code&gt;docker compose up&lt;/code&gt;, nginx is ready in a second but Traccar needs around 30 seconds. Your first &lt;code&gt;fetch&lt;/code&gt; gets a 502 from nginx. Poll a cheap endpoint such as &lt;code&gt;GET /api/server&lt;/code&gt; until it answers before signing in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using an API token?&lt;/strong&gt; The WebSocket still needs a session. Traccar can open one from a token: &lt;code&gt;GET /api/session?token=YOUR_TOKEN&lt;/code&gt; (same origin, through the proxy) sets the cookie, then connect as above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before production
&lt;/h2&gt;

&lt;p&gt;The config above is for local development. Before you expose it: serve HTTPS (nginx with Let's Encrypt, or Caddy in front), drop the &lt;code&gt;no-store&lt;/code&gt; header for static files, never ship credentials in the frontend, and keep Traccar's port 8082 off the public internet so the proxy is the only way in.&lt;/p&gt;

&lt;p&gt;That's the whole pattern: one origin, one proxy, relative URLs. Traccar stays untouched, and the browser does the cookie handling for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was written with the help of an AI assistant. Every config and code sample is copied from a setup tested with Traccar 6.15.3 and nginx 1.27 in Docker, and checked against Traccar's API documentation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>traccar</category>
      <category>nginx</category>
      <category>websocket</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
