Files
peardock-website/learn/index.html
T
2026-07-16 04:55:26 -04:00

583 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>How PearDock Works · P2P Docker control</title>
<meta name="description" content="Learn how PearDock manages Docker over peer-to-peer HyperDHT. No open ports, no VPN, no public Docker API. Identity, hole punching, control plane, Holesail tunnels, and security." />
<meta name="keywords" content="PearDock P2P, HyperDHT, hole punching, Holesail, Docker control plane" />
<meta name="author" content="HoneyPeer, LLC" />
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
<meta name="googlebot" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
<meta name="theme-color" content="#2dd4bf" />
<meta name="color-scheme" content="dark light" />
<meta name="application-name" content="PearDock" />
<meta name="apple-mobile-web-app-title" content="PearDock" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="msapplication-TileColor" content="#2dd4bf" />
<meta name="msapplication-config" content="/assets/favicons/browserconfig.xml" />
<meta name="format-detection" content="telephone=no" />
<link rel="canonical" href="https://peardock.boats/learn/" />
<link rel="alternate" hreflang="en" href="https://peardock.boats/learn/" />
<link rel="alternate" hreflang="x-default" href="https://peardock.boats/learn/" />
<!-- Open Graph -->
<meta property="og:type" content="article" />
<meta property="og:site_name" content="PearDock" />
<meta property="og:locale" content="en_US" />
<meta property="og:url" content="https://peardock.boats/learn/" />
<meta property="og:title" content="How PearDock Works · P2P Docker control" />
<meta property="og:description" content="Learn how PearDock manages Docker over peer-to-peer HyperDHT. No open ports, no VPN, no public Docker API. Identity, hole punching, control plane, Holesail tunnels, and security." />
<meta property="og:image" content="https://peardock.boats/assets/brand/og-image.png" />
<meta property="og:image:secure_url" content="https://peardock.boats/assets/brand/og-image.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="PearDock: decentralized Docker management on HyperDHT" />
<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="How PearDock Works · P2P Docker control" />
<meta name="twitter:description" content="Learn how PearDock manages Docker over peer-to-peer HyperDHT. No open ports, no VPN, no public Docker API. Identity, hole punching, control plane, Holesail tunnels, and security." />
<meta name="twitter:image" content="https://peardock.boats/assets/brand/twitter-card.png" />
<meta name="twitter:image:alt" content="PearDock: decentralized Docker management" />
<!-- Icons & PWA -->
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicons/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="48x48" href="/assets/favicons/favicon-48x48.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicons/apple-touch-icon.png" />
<link rel="manifest" href="/assets/favicons/site.webmanifest" />
<link rel="mask-icon" href="/assets/logo/peardock-wordmark.svg" color="#2dd4bf" />
<!-- Article -->
<meta property="article:section" content="Learn" />
<meta property="article:author" content="HoneyPeer, LLC" />
<meta property="article:publisher" content="HoneyPeer, LLC" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "How PearDock Works \u00b7 P2P Docker control",
"description": "Learn how PearDock manages Docker over peer-to-peer HyperDHT. No open ports, no VPN, no public Docker API. Identity, hole punching, control plane, Holesail tunnels, and security.",
"url": "https://peardock.boats/learn/",
"isPartOf": {
"@type": "WebSite",
"name": "PearDock",
"url": "https://peardock.boats"
},
"publisher": {
"@type": "Organization",
"name": "HoneyPeer, LLC",
"url": "https://peardock.boats"
},
"inLanguage": "en-US",
"primaryImageOfPage": {
"@type": "ImageObject",
"url": "https://peardock.boats/assets/brand/og-image.png",
"width": 1200,
"height": 630
}
}
</script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
<link rel="stylesheet" href="/src/styles/learn.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<header class="learn-hero">
<div class="learn-hero-inner">
<div class="badge mb-4">learn · how it works</div>
<h1>Peer-to-peer Docker control</h1>
<p class="lead">
PearDock is decentralized Docker management on the Holepunch stack.
<strong>You never need to open ports, expose Docker, or worry about remote access again.</strong>
You only need keys, peers, and the Engine you already run.
</p>
<div class="learn-key">
<div>
<strong>Key concept</strong>
Your laptop and a remote Docker host talk directly over an encrypted P2P stream.
No inbound firewall rules. No public <code>dockerd</code>. No always-on VPN.
Discovery and NAT traversal use HyperDHT. Docker commands ride protomux-rpc on that stream.
</div>
</div>
</div>
</header>
<div class="container learn-page">
<ul class="learn-toc">
<li><a href="#why">Why P2P?</a></li>
<li><a href="#problem">Old model</a></li>
<li><a href="#identity">Identity</a></li>
<li><a href="#connect">Connecting</a></li>
<li><a href="#nat">NAT &amp; hole punching</a></li>
<li><a href="#control">Control plane</a></li>
<li><a href="#data">Data plane</a></li>
<li><a href="#security">Security</a></li>
<li><a href="#fleet">Fleet</a></li>
<li><a href="#flow">End-to-end</a></li>
<li><a href="#glossary">Glossary</a></li>
</ul>
<!-- WHY -->
<section class="learn-section" id="why">
<h2>Why peer-to-peer for Docker?</h2>
<p class="learn-lead">
Docker Engine is powerful locally. Remotely, people usually open a TLS API port, maintain a VPN, or enroll hosts in a hosted panel.
PearDock takes another path: treat the host as a peer you reach with a public key.
<strong>No open ports, no exposed Engine, no remote-access checklist that never ends.</strong>
</p>
<p>
You can manage a home lab behind CGNAT, a VPS without punching firewall holes, or a fleet of edge machines.
Peers need to complete HyperDHT hole punching (or a relay fallback), and you need the right key material.
</p>
</section>
<!-- PROBLEM -->
<section class="learn-section" id="problem">
<h2>The traditional remote-Docker model</h2>
<p class="learn-lead">Most “remote Docker” setups trade convenience for a large attack surface or a permanent middleman.</p>
<div class="learn-compare">
<div class="learn-card learn-card--old">
<h4>✕ Centralized / exposed</h4>
<ul>
<li>Expose <code>dockerd</code> TCP + TLS to the internet</li>
<li>Always-on VPN just to run <code>docker ps</code></li>
<li>Hosted panel with your hosts enrolled forever</li>
<li>Single vendor outage = no ops UI</li>
</ul>
</div>
<div class="learn-card learn-card--new">
<h4>✓ PearDock peer model</h4>
<ul>
<li>Never open inbound ports for Docker or the control UI</li>
<li>Clients dial the <strong>public key</strong> over HyperDHT</li>
<li>Noise-encrypted RPC end to end. No VPN required.</li>
<li>You keep Docker and state on your machines</li>
</ul>
</div>
</div>
<pre class="mermaid">
flowchart LR
subgraph Traditional["Traditional"]
A1[Laptop] --> VPN[VPN / open :2376 / SaaS]
VPN --> D1[dockerd]
end
subgraph PearDock["PearDock"]
A2[Desktop client] -- "HyperDHT + Noise + protomux-rpc" --> S2[peardock-server]
S2 --> D2[dockerd]
end
</pre>
</section>
<!-- IDENTITY -->
<section class="learn-section" id="identity">
<h2>Identity: seeds and public keys</h2>
<p class="learn-lead">
PearDock v2 identity is a HyperDHT keypair derived from a secret seed.
It is not a shared “topic string” like some older swarm designs.
</p>
<div class="table-wrap">
<table>
<thead>
<tr><th>Material</th><th>Who has it</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr>
<td><code>SERVER_SEED</code></td>
<td>Server only (secret)</td>
<td>32-byte seed; generates the listening keypair. Never share.</td>
</tr>
<tr>
<td><code>SERVER_PUBLIC_KEY</code></td>
<td>Operators / clients</td>
<td>64 hex chars. Paste into the client to dial this host.</td>
</tr>
<tr>
<td>Client keys</td>
<td>Each desktop install</td>
<td>Peer identity for ACL, invites, and reconnect cache.</td>
</tr>
</tbody>
</table>
</div>
<p>
On first start the server writes <code>SERVER_SEED</code> and <code>SERVER_PUBLIC_KEY</code> to
<code>/opt/peardock/.env</code> (mode 600) and prints the public key in the journal.
<strong>Check the <code>.env</code> file</strong> for both values before connecting.
Rotating the seed changes the public key. All clients must re-add the peer.
Back up that file like any other root secret.
</p>
<div class="callout warn">
<div class="callout-icon">!</div>
<div class="callout-body">
<strong>Not the same as a Holesail <code>hs://</code> URL.</strong>
The server public key is for PearDock <em>control</em> RPC.
Tunnel capabilities are separate secrets created per published port.
</div>
</div>
</section>
<!-- CONNECT -->
<section class="learn-section" id="connect">
<h2>How a client finds and connects to a server</h2>
<p class="learn-lead">
Under the hood this is classic Holepunch connectivity: announce, look up, hole-punch (or relay), then encrypt.
PearDocks application layer starts only after that secure stream exists.
</p>
<div class="learn-peers">
<div class="learn-peer">
<h4>Client</h4>
<p>Desktop app holds public key + optional invite</p>
</div>
<div class="learn-arrow"><span></span>lookup</div>
<div class="learn-peer learn-peer--hub">
<h4>HyperDHT</h4>
<p>Distributed peer discovery &amp; signaling</p>
</div>
<div class="learn-arrow"><span></span>announce</div>
<div class="learn-peer">
<h4>Server</h4>
<p>Listens on keypair next to Docker</p>
</div>
</div>
<div class="learn-steps">
<div class="learn-step">
<h4>Announce</h4>
<p>The server registers its presence on HyperDHT under its cryptographic identity so dialers can find a path.</p>
</div>
<div class="learn-step">
<h4>Lookup &amp; punch</h4>
<p>The client queries the DHT, then both sides attempt coordinated UDP probes so NAT mappings open (“hole punching”).</p>
</div>
<div class="learn-step">
<h4>Encrypt &amp; RPC</h4>
<p>A Noise-encrypted stream carries protomux-rpc methods and server→client pushes (stats, logs, events).</p>
</div>
</div>
<pre class="mermaid">
sequenceDiagram
participant C as Client
participant DHT as HyperDHT
participant S as peardock-server
participant D as dockerd
S->>DHT: announce identity
C->>DHT: lookup public key
DHT-->>C: candidate endpoints
C->>S: hole punch / connect
C->>S: Noise handshake
C->>S: protomux-rpc handshake
S-->>C: role · protocol version
C->>S: listContainers / deploy / …
S->>D: dockerode
D-->>S: result
S-->>C: response + optional pushes
</pre>
</section>
<!-- NAT -->
<section class="learn-section" id="nat">
<h2>NAT traversal &amp; hole punching</h2>
<p class="learn-lead">
Most devices sit behind Network Address Translation. Routers map private IPs to public ports and block unsolicited inbound traffic.
That is why “just open Docker on the internet” is a bad default, and why P2P needs help to dial home.
</p>
<h3>What hole punching does</h3>
<ol>
<li><strong>Both peers learn public endpoints</strong> via DHT-assisted signaling (not a single app-specific rendezvous you host).</li>
<li><strong>Simultaneous outbound probes</strong> create temporary NAT mappings toward each other.</li>
<li><strong>Direct path preferred</strong> when mappings cooperate (common on home NAT).</li>
<li><strong>Relay fallback</strong> may apply when both sides are hard symmetric NAT. Traffic stays encrypted. Relays do not get plaintext Docker commands.</li>
</ol>
<div class="learn-compare">
<div class="learn-card">
<h4>Easier NAT</h4>
<p>Open / consistent mappings → hole punching usually succeeds quickly. Typical home routers often work well.</p>
</div>
<div class="learn-card">
<h4>Harder NAT / CGNAT</h4>
<p>Symmetric NAT or carrier-grade NAT can block direct paths. The stack may need more probes or a relay. Still no need to expose dockerd publicly.</p>
</div>
</div>
<p>
PearDock benefits from the same Holepunch/libudx generation of tools that power Hyperswarm and Holesail.
You dont configure port forwards for the control channel. You share a key.
That is the whole remote-access story: no open ports, no exposed Engine, no perpetual VPN.
</p>
</section>
<!-- CONTROL PLANE -->
<section class="learn-section" id="control">
<h2>Control plane: protomux-rpc over HyperDHT</h2>
<p class="learn-lead">
Once the encrypted stream is up, PearDock speaks a structured RPC protocol, not ad-hoc JSON lines.
That is how the UI stays rich: typed methods, roles, and live pushes.
</p>
<div class="learn-stack">
<div class="learn-layer">
<div class="learn-layer-name">UI</div>
<p>Desktop client (Electron / Pear) for containers, registry, stacks, templates, Swarm, tunnels, terminals, and the job tray.</p>
</div>
<div class="learn-layer">
<div class="learn-layer-name">protomux-rpc</div>
<p>Request/response methods + push channels. <code>PROTOCOL_VERSION</code> negotiated on connect (currently 3).</p>
</div>
<div class="learn-layer">
<div class="learn-layer-name">HyperDHT</div>
<p>Peer discovery, hole punch coordination, Noise transport under the app protocol.</p>
</div>
<div class="learn-layer">
<div class="learn-layer-name">handlers</div>
<p>Server modules for containers, images, volumes, networks, stacks, Swarm, vault, registry browser, image updates, tunnels…</p>
</div>
<div class="learn-layer">
<div class="learn-layer-name">dockerode</div>
<p>Talks to local <code>dockerd</code> (usually the unix socket; server user in the <code>docker</code> group).</p>
</div>
</div>
<h3>Client → server (examples)</h3>
<ul>
<li><code>handshake</code>, <code>ping</code>: session setup and liveness</li>
<li><code>listContainers</code>, <code>checkImageUpdates</code>, start/stop, <code>recreateContainer</code></li>
<li><code>deployContainer</code>, <code>deployStack</code>, <code>syncStackFromGit</code></li>
<li><code>getContainerLogs</code>, <code>startTerminal</code>, <code>containerStats</code></li>
<li>Registry vault + catalog/tags/manifest, Swarm, schedules, tunnels</li>
</ul>
<h3>Server → client pushes</h3>
<ul>
<li><code>push:containers</code>, <code>push:allStats</code>: live fleet view without spam polling</li>
<li><code>push:logs</code>, <code>push:terminalOutput</code>: streaming operator surfaces</li>
<li><code>push:pullProgress</code> / <code>push:pushProgress</code> / build progress: hybrid job-tray layer progress when the Engine streams events</li>
</ul>
<p>
Method ↔ role maps live in the shared protocol. Viewers can observe; operators mutate day-to-day;
admins get destructive and policy-level actions. See
<a href="/docs/api">RPC &amp; API</a> and <a href="/docs/security">Security</a>.
</p>
</section>
<!-- DATA PLANE -->
<section class="learn-section" id="data">
<h2>Data plane: Holesail tunnels (optional traffic path)</h2>
<p class="learn-lead">
Managing Docker is not the same as reaching a published web app or TCP service.
PearDock keeps a second plane for that: Holesail L4 tunnels.
Still <strong>no open host ports</strong>. Share an <code>hs://</code> key instead of a firewall rule.
</p>
<pre class="mermaid">
flowchart LR
subgraph Control["Control plane"]
UI[Client RPC]
SRV[Server handlers]
UI --> SRV
SRV --> DK[Docker Engine]
end
subgraph Data["Data plane"]
HC[Holesail client]
HS[HolesailServer]
HC -- "hs:// capability" --> HS
HS --> Port["host:published-port"]
end
Control -.->|"orthogonal"| Data
</pre>
<div class="table-wrap">
<table>
<thead>
<tr><th>Plane</th><th>Technology</th><th>Carries</th></tr>
</thead>
<tbody>
<tr>
<td><strong>Control</strong></td>
<td>HyperDHT + protomux-rpc</td>
<td>Docker ops, ACL, logs stream control, deploy, stats</td>
</tr>
<tr>
<td><strong>Data / tunnels</strong></td>
<td>Holesail</td>
<td>Raw TCP/UDP bytes to a published host port via <code>hs://</code></td>
</tr>
</tbody>
</table>
</div>
<p>
<strong>Why not use only Holesail?</strong>
Holesail moves bytes between sockets. It is not an RPC framework, role system, or audit log.
PearDock needs structured methods and live UI channels, so control stays on protomux-rpc.
</p>
<p>
Tunnels are <strong>on by default</strong> (<code>ENABLE_HOLESAIL=0</code> to disable).
Treat every <code>hs://</code> URL as a capability secret. Details:
<a href="/docs/holesail">Holesail guide</a>.
</p>
</section>
<!-- SECURITY -->
<section class="learn-section" id="security">
<h2>Security model (operator mental model)</h2>
<p class="learn-lead">P2P does not mean “open to the world.” It means “reachable by those who hold the right cryptographic handles.”</p>
<ul>
<li><strong>Transport encryption:</strong> Noise on the HyperDHT stream, not cleartext Docker API on :2375.</li>
<li><strong>Roles:</strong> viewer (default for public key alone), operator, and admin gate methods server-side.</li>
<li><strong>Keys in <code>.env</code>:</strong> after install, check <code>/opt/peardock/.env</code> for <code>SERVER_PUBLIC_KEY</code> and <code>SERVER_SEED</code>.</li>
<li><strong>Peer policy:</strong> optional allowlists, <code>pd1.</code> invites (no seed sharing), and revoke for multi-operator fleets.</li>
<li><strong>Rate limits and audit:</strong> blunt force protection and accountability for privileged calls.</li>
<li><strong>Registry vault + browser:</strong> credentials encrypted at rest (AES-GCM); remote catalog/tags/manifests via Registry HTTP API V2. UI: top-level <strong>Registry</strong> view.</li>
<li><strong>Tunnel SSRF guard:</strong> default allowlist for tunnel targets (loopback and published binds).</li>
<li><strong>Browse roots:</strong> host filesystem browse is default-deny and constrained.</li>
</ul>
<p>
You still must secure Docker itself: who is in the <code>docker</code> group, what images you run, and how secrets enter containers.
PearDock is a control surface. It inherits the power of the Engine socket it uses.
</p>
<p>Deep dive: <a href="/docs/security">Threat model &amp; hardening</a> · <a href="/legal/security">Vulnerability reporting</a></p>
</section>
<!-- FLEET -->
<section class="learn-section" id="fleet">
<h2>Multi-peer fleet UX</h2>
<p class="learn-lead">
One desktop client can keep many servers. Each peer is a separate HyperDHT identity with its own role and health.
</p>
<ul>
<li>Disk-backed peer cache under the client config directory</li>
<li>Saved hosts edited under <strong>Settings → Peers</strong>; server ACL/invites under <strong>Access</strong></li>
<li>Auto-reconnect (retry every 5s when a peer drops)</li>
<li>Environment tags (prod / staging / lab) for mental models</li>
<li>Multi-step job tray with hybrid image pull/push progress for long ops</li>
<li>Registry browser, image update checks, Portainer-style templates on the active peer</li>
</ul>
<p>
There is no “log into PearDock cloud.” There is only: which public keys did you add, and which of those peers are online.
</p>
</section>
<!-- END TO END -->
<section class="learn-section" id="flow">
<h2>End-to-end: first session</h2>
<p class="learn-lead">From zero to a live containers list. This is the path most operators take.</p>
<pre class="mermaid">
flowchart TB
A[Install peardock-server on Docker host] --> B["Read keys from /opt/peardock/.env"]
B --> C[Install desktop client]
C --> D[Add peer · key / seed / pd1 invite]
D --> E[Handshake · role · protocol version]
E --> F[listContainers · stats pushes · manage]
F --> G{Need app traffic?}
G -->|Yes| H[Create Holesail tunnel · share hs://]
G -->|No| I[Stay on control plane only]
</pre>
<ol>
<li>Install the server (one-liner or binary) on a machine with Docker.</li>
<li>Read <code>SERVER_PUBLIC_KEY</code> and (for admin) <code>SERVER_SEED</code> from <code>/opt/peardock/.env</code>.</li>
<li>Install the client; Add peer with key / key+seed / full <code>pd1.</code> invite; wait for health.</li>
<li>Operate: containers (updates), registry, templates, stacks, logs, terminal, Swarm, prune…</li>
<li>Optionally tunnel a published port with Holesail for users outside the control path.</li>
</ol>
<p>
Install: <a href="/download">Download &amp; install</a> ·
Guide: <a href="/docs/quickstart">Quick start</a> ·
Binaries: <a href="/releases">Releases</a>
</p>
</section>
<!-- GLOSSARY -->
<section class="learn-section" id="glossary">
<h2>Glossary</h2>
<div class="learn-glossary">
<details>
<summary>HyperDHT</summary>
<p>Distributed hash table used for peer announcements, lookups, and hole-punch coordination. Peers find each other by cryptographic identity without PearDock hosting a central directory of your fleet.</p>
</details>
<details>
<summary>Noise / Secret Stream</summary>
<p>Encrypted, mutually authenticated transport under HyperDHT connections. Application data (RPC) is not sent in the clear across the peer path.</p>
</details>
<details>
<summary>protomux-rpc</summary>
<p>RPC framework PearDock uses for named methods and server-initiated push channels over a multiplexed stream.</p>
</details>
<details>
<summary>Hole punching</summary>
<p>Technique where two NATed peers send coordinated outbound packets so temporary firewall mappings allow a direct path. If both sides are hostile NAT, encrypted relays may assist.</p>
</details>
<details>
<summary>NAT / CGNAT</summary>
<p>Network Address Translation (and carrier-grade NAT) share public IPs and block unsolicited inbound connections. That is why “open port 2376” is painful and unsafe as a default.</p>
</details>
<details>
<summary>Public key vs SERVER_SEED</summary>
<p>Both are written to <code>/opt/peardock/.env</code> on first start. The public key is what clients dial (viewer if used alone). The seed is secret server identity — paste it with the public key for admin only. Sharing the seed is equivalent to handing over admin.</p>
</details>
<details>
<summary>pd1. invite</summary>
<p>Self-contained peardock share string from Access → Create invite. Embeds public key + HMAC capability so operators connect without receiving <code>SERVER_SEED</code>. Paste the full string in Add peer (never truncate).</p>
</details>
<details>
<summary>Holesail / hs://</summary>
<p>Peer-to-peer L4 reverse proxy. An <code>hs://</code> URL is a capability to reach a specific tunnel endpoint. It is separate from PearDocks Docker control key.</p>
</details>
<details>
<summary>dockerode</summary>
<p>Node.js Docker client used by the PearDock server to talk to the local Docker Engine API (typically via the unix socket).</p>
</details>
<details>
<summary>Role (viewer / operator / admin)</summary>
<p>Server-enforced permission tier for RPC methods. UI may hide actions, but the server is the authority.</p>
</details>
<details>
<summary>Rolling release</summary>
<p>Continuously updated binary channel used by the installer and <a href="/releases">releases</a> page. Recommended for current peardock-server and peardock-client builds.</p>
</details>
</div>
</section>
<div class="learn-cta">
<h2>Ready to try it?</h2>
<p>Install a server next to Docker, paste the public key into the client, and operate without a central control plane.</p>
<div class="learn-cta-actions">
<a class="btn btn-primary" href="/download">Install PearDock</a>
<a class="btn btn-secondary" href="/docs/architecture">Architecture docs</a>
<a class="btn btn-ghost" href="/docs/">All documentation</a>
</div>
</div>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>