Skip to content

About

Your own private NVDA Remote relay server. One file, zero dependencies, deploy anywhere in seconds.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

NVDA Remote Server

Your own private NVDA Remote relay server. One file. Zero dependencies. Deploy anywhere in seconds.

A lightweight, single-file relay server for NVDA Remote Access -- the built-in remote control feature in NVDA 2025.1 and later. Run your own private server and keep your remote sessions under your control.

Remote Access was originally the NVDA Remote add-on, developed by Tyler Spivey and Christopher Toth. Starting with NVDA 2025.1, it ships as a built-in feature -- no add-on required. This server is compatible with both the built-in feature and the legacy add-on.

Why run your own server?

The default public NVDA Remote servers work, but they're shared infrastructure. Running your own server means:

  • Privacy -- your keystrokes and speech never touch a shared relay
  • Reliability -- no dependency on community-maintained servers
  • Control -- configure TLS, message limits, and MOTD on your terms
  • Low latency -- deploy in the region closest to you and your peers

This is especially valuable for blind system administrators who need reliable, private remote access to machines on their network.


Quick start

Pick your path. All roads lead to a running server.

Deploy on Railway

The fastest way to get a server running in the cloud. The full process takes about 5 minutes.

Deploy on Railway

Phase 1: Template initialization

Step 1: Click the Deploy button

Click the "Deploy on Railway" button above. This opens Railway's template creation page in a new tab.

Step 2: Authenticate and clone

Railway will ask you to connect your GitHub account if you aren't logged in already. It will prompt you to name the new repository it creates for you.

  • You can leave the default name or change it to something like nvda-rockstorm-server.
  • Check the box to make the repository Private. Even though we use environment variables for the certificate, privacy is a good default.

Step 3: Configure environment variables

Because of the railway.json file, Railway will present you with a form asking for your environment variables before it attempts to deploy.

Variable What to enter
NVDA_REMOTE_MOTD A message shown to users on connect (optional).
NVDA_REMOTE_CERT_CONTENT Leave blank to auto-generate a self-signed certificate on startup, or paste a base64-encoded PEM string (see Certificate via environment variable below).
NVDA_REMOTE_DEBUG false (unless you want raw JSON in the logs).
NVDA_REMOTE_PORT 6837

Step 4: Hit Deploy

Click the Deploy button at the bottom of the form. Railway will create the project, pull the Docker image, and start building.

Phase 2: TCP network routing

This is the one step the template cannot do for you. NVDA Remote uses raw TCP over TLS -- not HTTP -- so you need to enable Railway's TCP proxy. This can only be done through the Railway dashboard (there is no CLI command for it).

Step 1: Open the service settings

Once the project dashboard loads, you'll see a block representing your deployed service. Click on it.

Step 2: Navigate to Networking

In the panel that opens, select the Settings tab and find the Networking section.

Step 3: Enable TCP Proxy

Click on TCP Proxy and enter the internal port your service listens on. This is the port you set in NVDA_REMOTE_PORT (or 6837 if you left it at the default).

Step 4: Capture your server address

Railway will generate a proxy domain and port for you. It will look something like:

shuttle.proxy.rlwy.net:15140

Copy this address. The part before the colon is your host, and the number after the colon is your public port. This is what you'll enter in NVDA to connect.

Custom domains on Railway

You can point a custom domain at your Railway TCP proxy using a DNS CNAME record, but there's an important limitation to understand.

HTTP vs. TCP routing: When Railway hosts a regular website, it routes traffic by domain name, so custom domains work seamlessly on port 443. But NVDA Remote uses raw TCP. Railway handles this by putting you on a shared server with a random dedicated port (like 51106). DNS CNAME records only map names to names -- they can't remap ports.

Setting up the CNAME:

If Railway gave you shuttle.proxy.rlwy.net:15140, create a DNS record like this:

Type Name Target
CNAME remote shuttle.proxy.rlwy.net

Note: drop the port from the target -- DNS breaks if you include it.

Your users would then connect with:

  • Host: remote.yourdomain.com
  • Port: 15140

This gives you a branded hostname, but users still need to type the port number.

Want a completely clean address? If your goal is remote.yourdomain.com with no port (just the default 6837), Railway's TCP proxy can't do that because it doesn't give you a dedicated IP. For a true port-free custom domain, run this on a VPS (DigitalOcean, Linode, Hetzner -- as cheap as $5/month) where you get your own IPv4 address and full control over which port listens.


Run with Docker

Prebuilt multi-arch images (amd64 + arm64) are published to GitHub Container Registry on every push to main.

Pull the image:

docker pull ghcr.io/a2hsh/nvda-remote:latest

Using Docker Compose (recommended):

The easiest way to run the server. Grab the compose file and go:

curl -O https://raw.githubusercontent.com/a2hsh/nvda-remote/main/docker-compose.yml
# Edit docker-compose.yml to set your environment variables, then:
docker compose up -d

Using docker run:

Minimal -- auto-generates a self-signed certificate:

docker run -d \
  -p 6837:6837 \
  --name nvda-remote \
  --restart unless-stopped \
  ghcr.io/a2hsh/nvda-remote:latest

With all the bells and whistles:

docker run -d \
  -p 6837:6837 \
  --name nvda-remote \
  --restart unless-stopped \
  -e NVDA_REMOTE_PORT=6837 \
  -e NVDA_REMOTE_MOTD="Welcome to my NVDA Remote server." \
  -e NVDA_REMOTE_CERT_CONTENT="$(base64 -w0 server.pem)" \
  ghcr.io/a2hsh/nvda-remote:latest

Pin to a specific version if you prefer stability over living on the edge:

docker run -d \
  -p 6837:6837 \
  --name nvda-remote \
  --restart unless-stopped \
  ghcr.io/a2hsh/nvda-remote:1.0.0

Building locally:

docker build -t nvda-remote .
docker run -d -p 6837:6837 --name nvda-remote --restart unless-stopped nvda-remote

Run directly

No Docker? No problem. Requires Python 3.8+ and OpenSSL.

python server.py

That's it. One file, zero pip install, pure Python standard library. The server will auto-generate a self-signed certificate on first run if none exists.


Configuration

All options can be set via command-line arguments or environment variables. Environment variables make it easy to configure in Docker, Railway, or any platform that supports them.

Argument Environment Variable Default Description
--port NVDA_REMOTE_PORT 6837 Port to listen on
--certfile NVDA_REMOTE_CERTFILE server.pem Path to TLS certificate file
--keyfile NVDA_REMOTE_KEYFILE server.pem Path to TLS key file
--motd NVDA_REMOTE_MOTD (none) Message of the day shown on join
--motd-force NVDA_REMOTE_MOTD_FORCE false Force-display the MOTD
--max-msg-size NVDA_REMOTE_MAX_MSG_SIZE 1048576 Max message size in bytes (0 = unlimited)
--debug NVDA_REMOTE_DEBUG false Enable debug logging
--tracebacks NVDA_REMOTE_TRACEBACKS false Log full tracebacks on errors
--generate-cert -- -- Generate a self-signed certificate and exit

TLS certificates

The server requires TLS -- every connection is encrypted. On first run, it will auto-generate a self-signed certificate (server.pem) using OpenSSL. For production, you have several options:

Provide your own certificate file:

python server.py --certfile /path/to/cert.pem --keyfile /path/to/key.pem

Generate a fresh self-signed certificate:

python server.py --generate-cert

Certificate via environment variable

On platforms like Railway where you can't upload files and the filesystem is ephemeral, you can inject a certificate entirely through an environment variable. The server will decode it and write the PEM file at startup -- no file uploads needed.

Step 1: Generate a certificate locally

python server.py --generate-cert

This creates server.pem in the current directory (a combined certificate + private key file).

Step 2: Base64-encode the certificate

# Linux / macOS
base64 -w0 server.pem

# Windows (PowerShell)
[Convert]::ToBase64String([IO.File]::ReadAllBytes("server.pem"))

Copy the output -- this is your certificate as a single string with no line breaks.

Step 3: Set the environment variable

On Railway, go to your project's Variables tab and add:

Variable Value
NVDA_REMOTE_CERT_CONTENT (paste the base64 string from step 2)

You can also set it with the Railway CLI:

railway variables set NVDA_REMOTE_CERT_CONTENT="$(base64 -w0 server.pem)"

Or in Docker:

docker run -d -p 6837:6837 \
  -e NVDA_REMOTE_CERT_CONTENT="$(base64 -w0 server.pem)" \
  ghcr.io/a2hsh/nvda-remote:latest

When the server starts, it decodes NVDA_REMOTE_CERT_CONTENT and writes it to the certificate path (default server.pem). This happens before the TLS listener starts, so the server is ready immediately.


Connecting with NVDA

As of NVDA 2025.1, Remote Access is built in. No add-on needed.

  1. In NVDA, go to Tools > Remote > Connect
  2. Select Control another machine or Allow this machine to be controlled
  3. Enter your server address -- for example:
    • proxy.rlwy.net:12345 -- Railway TCP proxy
    • remote.yourdomain.com:51106 -- custom domain on Railway
    • yourserver.example.com:6837 -- self-hosted (VPS, home server, etc.)
  4. Enter or generate a key
  5. Click Connect

Both users must connect to the same server with the same key.

Still on an older version of NVDA? The legacy NVDA Remote add-on works with this server too.


Architecture

This is a single-file asyncio server (~300 lines of Python) that acts as a message relay:

  • Clients connect over TLS and join a channel identified by a shared key
  • Messages from one client are broadcast to all other clients in the same channel
  • Supports both NVDA Remote protocol v1 and v2 clients simultaneously
  • Uses TCP keepalive for dead connection detection (no application-level ping overhead)
  • Per-client write queues with backpressure (queue full = connection closed)
  • Channels are created on first join and destroyed when the last client leaves

No database. No framework. No dependencies. Just Python and a socket.


Releasing new versions

The project uses a VERSION file as the single source of truth. To release:

  1. Update the version in VERSION
  2. Commit the change
  3. Run ./release.sh

The script will tag, push, and trigger the CI to build Docker images for the new version. Images are tagged as latest, X.Y.Z, X.Y, and X.


License

MIT -- see LICENSE for details.

About

Your own private NVDA Remote relay server. One file, zero dependencies, deploy anywhere in seconds.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages