Tesla’s Vehicle Command SDK includes an HTTPS proxy that signs vehicle commands. I deployed the official Docker image on Unraid for use with evcc and a Tesla Wall Connector Gen 3.
The difficult part was not starting Docker: it was connecting the application registration, account permissions, and vehicle key in the correct order.
Verification status: The proxy is running, its HTTPS certificate validates, Tesla’s API returns my vehicle, and the virtual key has been enrolled. The evcc certificate mapping and final charging-command test are follow-up steps below, not verified outcomes of this setup session.
What is needed
- Unraid with Docker and the Compose Manager plugin.
- evcc running on the same Unraid server using host networking.
- A Tesla developer application and access to the vehicle-owner account.
- A public HTTPS domain for hosting the application’s public key.
- The Tesla mobile app, signed in as the vehicle owner.
This setup uses Tesla’s Internet-based Fleet API, not Bluetooth. Self-hosting the proxy does not bypass Tesla’s API billing or quotas.
1. Create the Tesla developer application
Create an application in the Tesla developer dashboard. Save its Client ID and Client Secret privately.
For charging-only evcc use, enable:
| Permission | OAuth scope | Purpose |
|---|---|---|
| Vehicle Information | vehicle_device_data | Identify the vehicle and read battery/charging data |
| Vehicle Charging Management | vehicle_charging_cmds | Start/stop charging and set charging current |
The authorization request also includes openid and offline_access so the client can maintain access with refresh tokens. Broader Vehicle Commands and Vehicle Location permissions are not needed for this charging-only setup.
I used myTeslaMate’s OAuth callback to display the authorization code, while hosting my own command-signing key:
| Developer setting | Value used |
|---|---|
| Allowed Origin | https://dvkwong.com |
| Allowed Redirect URI | https://app.myteslamate.com/auth/tesla/user/callback |
| Allowed Returned URL | https://app.myteslamate.com/ |
The callback and key-hosting domain serve different purposes. The blog does not implement an OAuth callback. If using a different OAuth client, use its actual registered callback instead.
2. Store configuration on Unraid’s appdata share
Run the following commands in the Unraid terminal. The official image runs as UID/GID 65532.
mkdir -p /mnt/user/appdata/tesla-vehicle-command/configchown 65532:65532 /mnt/user/appdata/tesla-vehicle-command/configchmod 700 /mnt/user/appdata/tesla-vehicle-command/configDo not keep the keys in a relative ./config directory under the Compose Manager project on /boot. On this server, /boot is FAT with root-only permissions. The container failed with:
Error: open /config/fleet-key.pem: permission deniedAn absolute appdata bind mount avoids that filesystem-permission problem.
3. Generate separate signing and HTTPS keys
The signing key authenticates commands to the vehicle. The TLS key secures the proxy connection. They must be different keys.
Run once, before any key has been enrolled. Do not overwrite existing keys when repeating the setup.
(set -euumask 077cd /mnt/user/appdata/tesla-vehicle-command/configfor file in fleet-key.pem public-key.pem tls-key.pem tls-cert.pem; do if [ -e "$file" ]; then echo "STOP: $file already exists; preserve the existing keys." exit 1 fidone
openssl ecparam -name prime256v1 -genkey -noout -out fleet-key.pemopenssl pkey -in fleet-key.pem -pubout -out public-key.pem
openssl req -x509 -nodes -newkey ec \ -pkeyopt ec_paramgen_curve:secp384r1 \ -subj '/CN=unraid' \ -keyout tls-key.pem -out tls-cert.pem -sha256 -days 825 \ -addext 'subjectAltName=DNS:unraid,DNS:localhost,IP:127.0.0.1' \ -addext 'extendedKeyUsage=serverAuth' \ -addext 'keyUsage=digitalSignature'
chown 65532:65532 fleet-key.pem public-key.pem tls-key.pem tls-cert.pemchmod 600 fleet-key.pem tls-key.pemchmod 644 public-key.pem tls-cert.pem)Back up the keys securely. Replacing the signing key requires enrolling the new public key in the vehicle.
4. Configure and start the Docker proxy
In Compose Manager, create a Tesla Vehicle Command project using:
services: tesla_http_proxy: image: tesla/vehicle-command:latest restart: unless-stopped ports: - "127.0.0.1:4443:4443" environment: TESLA_HTTP_PROXY_TLS_CERT: /config/tls-cert.pem TESLA_HTTP_PROXY_TLS_KEY: /config/tls-key.pem TESLA_HTTP_PROXY_HOST: 0.0.0.0 TESLA_HTTP_PROXY_PORT: "4443" TESLA_HTTP_PROXY_TIMEOUT: 10s TESLA_KEY_FILE: /config/fleet-key.pem TESLA_VERBOSE: "false" security_opt: - no-new-privileges:true volumes: - /mnt/user/appdata/tesla-vehicle-command/config:/config:roUse Compose Up, then test from Unraid:
curl --cacert /mnt/user/appdata/tesla-vehicle-command/config/tls-cert.pem \ https://127.0.0.1:4443/api/1/vehiclesWithout an OAuth token, the expected result is HTTP 403 with client did not provide an OAuth token. This verifies the HTTPS listener, not vehicle access.
Only loopback is published. Do not expose port 4443 through a router or Cloudflare Tunnel. evcc can reach it because it uses host networking on the same server. A bridge-networked or remote evcc instance needs a different, secured network arrangement.
5. Publish only the public key
Display or copy this file:
cat /mnt/user/appdata/tesla-vehicle-command/config/public-key.pemHost its raw PEM contents at this exact path on your domain:
https://YOUR_DOMAIN/.well-known/appspecific/com.tesla.3p.public-key.pemFor my Astro blog, the source file is public/.well-known/appspecific/com.tesla.3p.public-key.pem, published at:
https://dvkwong.com/.well-known/appspecific/com.tesla.3p.public-key.pemCloudflare or any HTTPS static host can serve it. The response must be the public key, not HTML, a login page, or a bot challenge. Keep the URL available after enrollment.
Never publish fleet-key.pem, tls-key.pem, OAuth tokens, or the client secret.
6. Store OAuth application credentials privately
These credentials are used for token requests, not placed in the proxy’s environment.
umask 077mkdir -p /mnt/user/appdata/tesla-vehicle-command/oauthchmod 700 /mnt/user/appdata/tesla-vehicle-command/oauthnano /mnt/user/appdata/tesla-vehicle-command/oauth/client.jsonEnter your own values:
{ "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET"}This directory is outside the proxy’s mounted configuration. Do not commit it to Git or share its contents.
7. Register the public-key domain with Tesla
Publishing the key and adding Allowed Origins do not perform Fleet API partner registration.
The following requests a temporary partner token and registers the domain directly with Tesla. It uses the NA Fleet API endpoint, which was confirmed as the correct region for my New Zealand account. Other accounts should use the appropriate regional endpoint.
Change domain before running this on another installation:
(set -euumask 077cd /mnt/user/appdata/tesla-vehicle-command/oauthdomain=dvkwong.comapi=https://fleet-api.prd.na.vn.cloud.tesla.comwork=$(mktemp -d)trap 'rm -rf "$work"' EXIT
jq -rj --arg audience "$api" \ '{grant_type:"client_credentials",client_id,client_secret, scope:"openid vehicle_device_data vehicle_charging_cmds", audience:$audience} | to_entries | map((.key|@uri)+"="+(.value|@uri)) | join("&")' \ client.json > "$work/request"
curl --fail-with-body --silent --show-error --max-time 45 \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-binary @"$work/request" \ https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token \ -o "$work/token"
jq -er '"Authorization: Bearer " + (.access_token | select(length > 0))' \ "$work/token" > "$work/header"jq -n --arg domain "$domain" '{domain:$domain}' > "$work/registration"
curl --fail-with-body --silent --show-error --max-time 45 \ -H @"$work/header" -H 'Content-Type: application/json' \ --data-binary @"$work/registration" \ "$api/api/1/partner_accounts" -o "$work/result"jq '{response,error,error_description}' "$work/result")Successful registration returned HTTP 200 with domain: dvkwong.com. The published public key was also checked against the proxy’s private key.
Use jq -rj, not just jq -r, when creating the form body: a trailing newline in the final audience value caused an invalid_audience error during setup.
Do not rerun myTeslaMate’s full registration script for this self-hosted domain. Its registration step targets a myTeslaMate hostname, which produced a root-domain/Allowed Origin mismatch in this setup.
8. Authorize the vehicle-owner account
A partner token registers the application; it is not a user access token. The vehicle owner must separately grant access.
Generate a login URL on Unraid:
(set -euumask 077cd /mnt/user/appdata/tesla-vehicle-command/oauthstate=$(openssl rand -hex 32)printf '%s' "$state" > authorization-statejq -r --arg state "$state" \ '"https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/authorize?" + ({client_id,redirect_uri:"https://app.myteslamate.com/auth/tesla/user/callback", response_type:"code", scope:"openid offline_access vehicle_device_data vehicle_charging_cmds", state:$state,prompt:"consent"} | to_entries | map((.key|@uri)+"="+(.value|@uri)) | join("&"))' \ client.json)Open that URL, sign in with the same owner account used in the Tesla app, select the vehicle, and grant Vehicle Information and Vehicle Charging Management. For a self-hosted callback, validate the returned state against the stored value; this walkthrough uses myTeslaMate’s callback/code-display flow.
If no consent screen appears and the required permissions remain missing, revoke only this application under the Tesla account’s Security → Third-Party Apps, then start a fresh authorization flow. Revocation invalidates existing tokens for that application.
Exchange the authorization code immediately
Codes expire quickly and work only once. Run this on Unraid and paste only the newly issued code at the hidden prompt:
(set -euumask 077cd /mnt/user/appdata/tesla-vehicle-command/oauthwork=$(mktemp -d)trap 'rm -rf "$work"' EXITread -r -s -p 'New authorization code: ' codeprintf '\n'printf '%s' "$code" > "$work/code"unset code
jq -rj --rawfile code "$work/code" \ '{grant_type:"authorization_code",client_id,client_secret, code:$code,redirect_uri:"https://app.myteslamate.com/auth/tesla/user/callback"} | to_entries | map((.key|@uri)+"="+(.value|@uri)) | join("&")' \ client.json > "$work/request"
curl --silent --show-error --max-time 45 \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-binary @"$work/request" \ https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token \ -o "$work/response"
if jq -e '(.access_token|type=="string" and length>0) and (.refresh_token|type=="string" and length>0)' \ "$work/response" >/dev/null; then install -m 600 "$work/response" tokens.json rm -f authorization-state echo 'SUCCESS: OAuth tokens saved privately.'else jq '{error,error_description}' "$work/response" exit 1fi)A successful exchange does not prove that all requested permissions were granted. Initially, my token contained charging permission only; listing vehicles returned 403 Unauthorized missing scopes. Reauthorizing with Vehicle Information fixed vehicle access.
9. Enroll the virtual key in the Tesla app
Do this after account consent, not before.
- Wake the vehicle and select it in the Tesla app.
- On the phone, open
https://tesla.com/_ak/dvkwong.com(replace the domain for another installation). - Approve the virtual-key request.
- Check Controls → Locks in the vehicle for the new key.
Before account consent was complete, the app reported:
Adding a virtual key for "dvkwong.com" is forbidden because you have not granted "dvkwong.com" access to your account.The key was successfully enrolled after correcting the OAuth consent flow. No new signing key was needed.
10. Let evcc trust the proxy certificate
In Unraid, choose evcc → Edit → Add another Path, Port, Variable, Label or Device, then add a Path:
| Setting | Value |
|---|---|
| Host path | /mnt/user/appdata/tesla-vehicle-command/config/tls-cert.pem |
| Container path | /etc/ssl/certs/tesla-proxy.pem |
| Access mode | Read Only |
Apply the change and restart evcc. This mount is intended to make the self-signed certificate available in the Alpine-based container’s certificate directory; verify actual trust with a charging request before considering integration complete. Do not disable TLS verification or mount either private key into evcc.
Persist the mapping in the Unraid template, not with a one-off docker cp, so container recreation retains it.
11. Configure the vehicle and loadpoint in evcc
Open Configuration → Vehicles → Add → Tesla, not Tesla BLE.
| Field | Value |
|---|---|
| Client ID | Your Tesla application’s Client ID |
| Access token | Latest user OAuth access token |
| Refresh token | Matching user OAuth refresh token |
| VIN | Your vehicle’s VIN |
| Command Proxy, under advanced settings | https://127.0.0.1:4443 |
| Proxy Token | Leave empty; this is for the paid myTeslaMate proxy |
To display the saved tokens only in your own Unraid terminal:
jq '{access_token, refresh_token}' \ /mnt/user/appdata/tesla-vehicle-command/oauth/tokens.jsonCopy them into evcc; do not share screenshots or paste them into chat. evcc should manage subsequent token refreshes. The manually saved file is a bootstrap snapshot, not an automatically synchronized copy of evcc’s current tokens.
Then open Configuration → Loadpoints → Tesla Charger → Edit, select the newly added vehicle, and save. Keep charging mode Off until testing is ready.
12. Verify before enabling automation
- Confirm the container stays running without a restart loop.
- Confirm HTTPS works with certificate verification enabled.
- Confirm evcc can read the correct vehicle’s battery level.
- With the car plugged in and ready, deliberately test charging start/stop and current adjustment.
- Check evcc and proxy logs for TLS, OAuth, or key-pairing errors.
Reading battery data verifies OAuth access, but does not verify signed commands. Only a successful charging command completes the end-to-end check. At the time of writing, virtual-key enrollment and Fleet API vehicle access had succeeded; this final evcc charging test was still outstanding.
