SDKs and platforms

Can I run Offline Protocol on a server or in Docker?

Yes. The Python package runs a full peer on Linux servers, and its offline-protocol-service command serves one engine to local programs over a local API, with an optional HTTP front for programs that do not use the SDK. The SDK repository has a Dockerfile for that service, with a published image for linux/amd64 and linux/arm64, and hosts without a keyring keep their keys in the SDK's file stores under a store key. Java, C, Ruby and the Rust crates also run on servers, with your process supplying the transports.

TL;DR

Offline Protocol runs on Linux servers through the Python package, whose offline-protocol-service shares one engine with local programs and can add an HTTP front. The SDK repository builds a container image for it, also published on GitHub Container Registry. Headless hosts keep keys in the file stores under a store key, and Java, C, Ruby and Rust run there too.

Learning objectives

After reading this article you will be able to:

  • Run offline-protocol-service on a Linux host and decide how local programs reach it
  • Keep a headless host's identity in the built-in file stores under a store key
  • Build or pull the service container and give it the grants it needs

Offline Protocol on servers

Offline Protocol runs on Linux servers through the bindings built for desktop and server processes. As of 0.31.0 (October 2026):

BindingOn a serverTransports
Python, offline-protocol-sdk (beta)Wheels for Linux x86_64 and aarch64 with glibc 2.34 or laterShips its own: internet relay, LAN peer streams, Bluetooth LE, gateway client
Python service, offline-protocol-serviceInstalled with the package; also a container imageStarts every transport its configuration enables
Java, offline-protocol-javaJDK 22 or later, over the release’s Linux libraryYour process supplies them
C and languages that call Coffline_protocol.h and the release’s Linux libraryYour process supplies them
Ruby, offline_protocolRubyGems, over the release’s Linux libraryYour process supplies them
Rust, offline-protocolcrates.ioYou write the platform bridge

Python is the shortest path, because it is the only one of these that ships transport bridges. With the others, your code drains each transport’s outbound queue, does the I/O and reports the result.

The SDK is AGPL-3.0-only, including its network-use source-disclosure requirement, or available under a commercial licence for organisations that cannot meet it, such as a SaaS that does not release its source. The licensing docs have the terms.

Python SDK on a Linux server

The Python package runs a full peer on any Linux x86_64 or aarch64 server with glibc 2.34 or later, which the docs equate with Debian 12, Ubuntu 22.04, RHEL 9 or newer. There is no wheel for musl-based Linux such as Alpine or for an older glibc, and no source distribution, so those hosts build from source.

A host that runs the service wants both extras: lan finds other hosts on the LAN over DNS-SD, and http adds the HTTP front.

pip install 'offline-protocol-sdk[lan,http]'

How do I use Offline Protocol from Python? covers the API itself.

Store key for hosts without a keyring

A server or container usually has no secret service, so the keyring cannot hold the device’s keys. On a headless Linux host keyring falls back to a backend that cannot hold keys, and the SDK logs a warning. Pass a store key instead, and the SDK uses its built-in file stores, sealing every MLS record on disk under that key:

pm = ProtocolManager(
    config,
    store_key_env="OFFLINE_PROTOCOL_STORE_KEY",  # 64 hex digits or base64
    mls_root="/var/lib/example/keys",            # or OFFLINE_PROTOCOL_MLS_ROOT
    state_root="/var/lib/example/state",         # or OFFLINE_PROTOCOL_STATE_ROOT
)
await pm.start()

The rules that matter in operation:

  • The key is 32 random bytes, generated once and kept. Generate it with openssl rand -hex 32 and hand it over the way the service manager hands over any secret. Never derive it from a password or a host name.
  • mls_root is the device’s identity. Losing it, or the key, gives the device a new address. Keep mls_root and state_root apart; the same directory, or one inside the other, is refused.
  • Moving a keyring deployment onto the file stores starts a new identity. Point state_root at a fresh directory; a state directory written by another identity is refused.
  • start() raises rather than run without the identity. A wrong key raises ProtocolError.InvalidConfiguration and changes no record.

On a host without a keystore the store key sits wherever the process reads it from, which the SDK’s threat model records as residual risk R18. Java, C and Ruby reach the same stores through initialize_mls_with_file_stores, and Rust through FileStorePair::open with EnvStoreKey, which reads the same environment variable.

Local API service with offline-protocol-service

offline-protocol-service runs one engine and serves any number of local programs over JSON-RPC 2.0 on a WebSocket, so several programs on a host share one identity instead of each running a node. A client sends hello with its application id and then calls the engine’s methods by name.

export OFFLINE_PROTOCOL_STORE_KEY="$(openssl rand -hex 32)"   # once; keep it
offline-protocol-service --config config.json \
    --mls-root /var/lib/example/keys --state-root /var/lib/example/state \
    --socket /run/example/api.sock --listen 0.0.0.0:7878 --peer 10.0.0.2:7878

The default carrier is a Unix socket with mode 0600; --tcp PORT --token-file PATH serves loopback TCP with a token that is new at every launch. The socket is the authorization boundary, and the server does not tell one local process from another. The local API never binds a non-loopback address, so reaching it across a network takes a reverse proxy with its own authentication. Other Offline Protocol hosts reach this one over the peer stream on port 7878, not through the API.

A transport that fails to start is logged and the others run. offline-protocol-verify drives the running service and waits for the events that show a message was held, carried and delivered, which makes it a deployment check.

HTTP front for programs without the SDK

With the http extra, --http 127.0.0.1:8080 starts the HTTP front in the same process. A program that does not import the SDK then calls a service on another device with an ordinary HTTP request to <service>.<device>.offline.protocol.internal, carried inside end-to-end encrypted messages. How can a program call an HTTP service on another device with no internet? explains the approach.

The front does not authorize callers. Any device whose message the engine decrypts can call every registered service, so the provider decides whom it serves, for example by checking X-Offline-Protocol-Sender against addresses it received out of band. Off loopback, the front requires a per-launch token. Bodies are limited to 128 KiB each way, and a 504 does not mean the request did not run.

Offline Protocol in Docker

The SDK repository’s bindings/python/docker directory builds one service per host on python:3.12-slim, with the lan and http extras installed from PyPI and the front on 127.0.0.1:8080. From that directory, on a Linux host:

export OFFLINE_PROTOCOL_STORE_KEY="$(openssl rand -hex 32)"   # once per host; keep it
docker compose up -d

The container README at 0.31.0 also lists a published image, built from that directory for linux/amd64 and linux/arm64:

docker pull ghcr.io/offline-protocol/offline-protocol-service:0.31.0

Pin a deployment to the image digest rather than the tag, because a registry tag can be moved. Whatever runs the container owes it these grants:

GrantWhy
The host’s network namespaceMulticast DNS does not leave a container bridge, and the front then sits on the host’s loopback. Set OP_LISTEN to the host’s reserved LAN address.
Inbound TCP 7878Other hosts open their peer streams here.
A persistent volume at /var/lib/offline-protocolThe identity and every queued message. Without it the host has a new address after every recreate.
OFFLINE_PROTOCOL_STORE_KEY from the host’s secret storeEverything on the volume is sealed under it.
The system D-Bus socket, for Bluetooth LEThe service advertises to phones through BlueZ.
A clockA provider drops a request more than 60 seconds past its deadline.

On Docker Desktop for macOS and Windows, host networking is the namespace of Docker’s Linux virtual machine, so the front is not on the desktop’s loopback and multicast DNS does not reach its LAN. Use the SDK’s demos there and a Linux host for deployment.

The README describes a run of the image: two containers on one bridge network found each other over multicast DNS with no peer list, and served a request through the HTTP front from one to a provider on the other, end to end encrypted, in 35 ms. The address and the front’s registrations survived a restart of the container.

Rust, Java and C on servers

The Rust crates, the Java binding and the C API run on servers as engines without transports. They suit a server process that already owns its sockets and wants the protocol engine in-process rather than over the local API.

Frequently asked questions

Is there a published Docker image for Offline Protocol?

Yes. The container README at 0.31.0 lists ghcr.io/offline-protocol/offline-protocol-service, built for linux/amd64 and linux/arm64 from the SDK repository's Dockerfile. Pin the image by digest in a deployment, because version tags can be moved.

Can I expose the local API to other machines?

Not directly. The local API never binds a non-loopback address. To reach it across a network, put a reverse proxy with its own authentication in front of it. Other Offline Protocol hosts reach this one over the peer stream, not the API.

What happens if I lose the store key?

The identity is lost with it. Everything in the file stores is sealed under the key, so the device comes back with a new address, and its sessions and queued messages stay with the old one. Keep the key in the host's secret store, not beside the data.

Does Offline Protocol run on Alpine Linux?

Not from PyPI. There is no wheel for musl-based Linux such as Alpine and no source distribution, so pip finds nothing to install. Build from source there, or use a glibc-based image such as the SDK's own, which starts from python:3.12-slim.

Does the AGPL apply when I run the SDK on a server?

The SDK is AGPL-3.0-only, including the network-use source-disclosure requirement in section 13, or available under a commercial licence. Organisations that cannot meet the AGPL, for example in a SaaS that does not release its source, use the commercial licence.

Sources

Build it with Offline Protocol

The local API page covers starting the service, the transports it starts, the carriers, the policy file and the verifier.

Read the local API service docs