This is the shortest Docker-first path for a controlled Linux beta deployment.
It uses one FPS server, one Linux client, a carrier origin and an optional Dante
SOCKS5 overlay. The deterministic path uses self-hosted fps_carrier; real
deployments can also use browser/application sessions to an external origin as
described in real-origin-carriers.md. Replace
example addresses and names before using the commands outside a lab.
docker build -t fps:local .
Optional smaller runtime image:
docker build -f Dockerfile.alpine -t fps:alpine .
Generate the server key pair:
docker run --rm fps:local fps_server --generate-server-keypair
Generate one UUID per client device or profile:
CLIENT_UUID="$(docker run --rm fps:local fps_client --generate-client-uuid)"
printf '%s\n' "$CLIENT_UUID"
Edit examples/docker/server/config/server.json:
server_private_key_base64 and server_public_key_base64;allowed_client_uuids;network.origin to your self-hosted carrier origin, for example
fps-carrier-origin:9443;tun.lease_pool, tun.server_address and ops.status_socket explicit.Validate before starting:
docker run --rm -v "$PWD/examples/docker/server/config:/etc/fps:ro" fps:local \
fps_server --check-config --config /etc/fps/server.json
The server example publishes container port 8443. For a public beta-like
setup, bind it to host port 443:
cd examples/docker/server
FPS_PUBLISHED_PORT=443 docker compose up -d
docker compose ps
docker compose run --rm --no-deps fps-server status
For deterministic carrier traffic without an external origin, run
fps_carrier origin next to the server. The debug-carrier compose file is a
compact end-to-end example; production deployments can copy the same service
into their server compose stack.
cd examples/docker/debug-carrier
docker compose up -d fps-carrier-origin fps-server
docker compose run --rm --no-deps fps-server status
Generate a JSON profile from the server config:
docker run --rm -v "$PWD/config:/etc/fps:ro" fps:local \
fps_server --generate-client-profile \
--config /etc/fps/server.json \
--client-uuid "$CLIENT_UUID" \
--server-endpoint fps.example.net:443 \
--client-listen 127.0.0.1:443 \
--client-status-socket /run/fps/client.status \
--format json \
--output /tmp/client.json
Or generate a URI for transport:
docker run --rm -v "$PWD/config:/etc/fps:ro" fps:local \
fps_server --generate-client-profile \
--config /etc/fps/server.json \
--client-uuid "$CLIENT_UUID" \
--server-endpoint fps.example.net:443 \
--client-listen 127.0.0.1:443 \
--format uri
On the client, import the URI into a secret config file:
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/config:/etc/fps" fps:local \
fps_client --write-config-from-uri 'fps://v1/...' \
--output /etc/fps/client.json
Generated client profiles and fps://v1 URIs contain the client UUID bearer
secret. Treat them like passwords.
Use the host-network compose example when the client should create the TUN interface in the host namespace:
cd examples/docker/client-host
docker compose up -d
docker compose ps
docker compose run --rm --no-deps fps-client status
The server assigns the client TUN address after Zero-RTT authentication. Extra split/full routes are operator policy and are not pushed by the profile.
For real-origin browser/application carrier sessions, map the carrier hostname to the local client listener:
echo '127.0.0.1 carrier.example.net' | sudo tee -a /etc/hosts
Then open the normal carrier URL in a browser or carrier-capable application:
https://carrier.example.net/
wss://carrier.example.net/
Do not open https://127.0.0.1/ for a hostname-based origin: that commonly
breaks TLS certificates, SNI, Host and CORS behavior. DNS and /etc/hosts do
not select ports, so default HTTPS/WSS requires the local FPS client to listen
on 127.0.0.1:443.
For a deterministic debug carrier process instead of a browser:
docker run --rm fps:local fps_carrier client \
--connect 127.0.0.1:443 \
--path /fps-carrier \
--client-bps 4096 \
--frame-rate 4
Carrier liveness is required for VPN/SOCKS liveness. A leased TUN address means the client authenticated and received an address; it does not mean the tunnel is currently usable if every carrier has closed.
For browser or application carriers against a real origin, see
real-origin-carriers.md. It covers hostname
mapping, persistent WebSocket carriers, long-lived TLS application sessions and
curl --resolve sanity checks without depending on a public test service.
The same carrier-hostname override can be centralized on a LAN router: run
fps_client on the router, make it listen on the router LAN address, and
configure router DNS to resolve selected carrier hostnames to that LAN address.
This gives the LAN one FPS entry point, while carrier-capable devices still need
to keep carrier sessions open. See
real-origin-carriers.md.
Check status on both sides:
docker compose run --rm --no-deps fps-server status
docker compose run --rm --no-deps fps-client status
Expected signals:
auth.authenticated and sessions.carriers_current are non-zero;fpsc0;If tun.leased_client_address is present but sessions.carriers_current is
zero, open a persistent carrier before debugging routes, DNS or proxy settings.
For application proxy mode, use the official Dante overlay example:
docker build -f examples/docker/proxy-dante/Dockerfile \
--build-arg FPS_BASE_IMAGE=fps:local \
-t fps-dante-proxy:local .
cd examples/docker/server
docker compose -f compose.yml -f ../proxy-dante/compose.yml up -d
Applications can then use socks5h://10.66.0.1:1080 after the client receives
its TUN lease.
Minimal SOCKS smoke checks:
curl --socks5-hostname 10.66.0.1:1080 https://api.ipify.org?format=json
curl --socks5-hostname 10.66.0.1:1080 https://www.example.com/
The Dante overlay example has no SOCKS authentication or per-user ACLs. Keep it
bound to the FPS server TUN address and keep FPS_SOCKS_ALLOWED_CIDR as narrow
as the deployment allows.
Stop containers on each host where compose is running:
docker compose down -v --remove-orphans
For the official Dante overlay, include the overlay file:
docker compose -f compose.yml -f ../proxy-dante/compose.yml down -v --remove-orphans
Remove the hosts entry when it is no longer needed:
sudo sed -i '/carrier.example.net/d' /etc/hosts
For two-host tests, also remove temporary runtime directories on both machines, then verify that the public carrier port and TUN devices disappeared:
ss -ltn sport = :443
ip link show fpsc0 2>/dev/null || true
ip link show fpss0 2>/dev/null || true
Review linux-routing.md before adding split or full-tunnel routes, and rotation.md before reissuing UUIDs or server keys.