Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .icons/selkies.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions registry/selkies-project/.images/avatar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions registry/selkies-project/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
display_name: "Selkies"
bio: "Low-latency, GPU-accelerated web remote desktop streaming for self-hosting, containers, Kubernetes, and cloud and HPC clusters."
avatar: "./.images/avatar.svg"
github: "selkies-project"
website: "https://selkies.io"
status: "community"
---

# Selkies

Low-latency, GPU-accelerated web remote desktop streaming for self-hosting, containers, Kubernetes, and cloud and HPC clusters, from [selkies-project/selkies](https://github.com/selkies-project/selkies).
88 changes: 88 additions & 0 deletions registry/selkies-project/modules/selkies/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
display_name: Selkies
description: A low-latency desktop streamed to the browser, with audio, gamepads, and GPU encoding
icon: ../../../../.icons/selkies.svg
verified: false
tags: [desktop, selkies, gpu, websocket]
---

# Selkies

Stream the workspace's desktop to the browser with [Selkies](https://github.com/selkies-project/selkies): low latency at high frame rates, audio in both directions, gamepads, and the GPU encoding the stream where the workspace has one. Coder authenticates the app and proxies it over one WebSocket, on a subdomain or a path.

```tf
module "selkies" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/selkies-project/selkies/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
desktop_environment = "xfce"
}
```

> [!IMPORTANT]
> The workspace needs a desktop installed, as the [`codercom/example-desktop`](https://hub.docker.com/r/codercom/example-desktop) image has.

![Selkies desktop in a Coder workspace](../../.images/selkies-desktop.png)

`desktop_environment` names a session installed in the workspace (`xfce`, `kde`, `lxqt`, `gnome`, `mate`) or gives a command to run; empty starts the workspace's default desktop. The module's variables are those of the [KasmVNC module](https://registry.coder.com/modules/coder/kasmvnc), so a template swaps one for the other or offers both.

## Selkies in the Image

Where the image carries Selkies and Xvfb, the module installs nothing and needs neither `sudo` nor network access, and `install_selkies = false` keeps it that way. Selkies publishes native packages, a Python wheel, and an AppImage; see its [native install guide](https://github.com/selkies-project/selkies/blob/main/docs/native.md).

```tf
module "selkies" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/selkies-project/selkies/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
desktop_environment = "xfce"
install_selkies = false
}
```

## Installing at Start

Otherwise, the module installs the release's native package, the distribution's Xvfb, and PulseAudio where the workspace has no sound server, as root or with passwordless `sudo`, on the distributions a release publishes packages for (for 2.0.0: Ubuntu 24.04 and 26.04, Debian 12 and 13, Fedora, RHEL 9 and its rebuilds, Alpine, and Arch Linux). `selkies_version` pins a release, and `release_url` points at a mirror laid out like GitHub's releases.

```tf
module "selkies" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/selkies-project/selkies/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
desktop_environment = "xfce"
selkies_version = "2.0.0"
release_url = "https://artifacts.example.com/selkies/releases"
}
```

## Wayland

`wayland = true` streams through Selkies' Wayland backend instead of an Xvfb. The desktop is then a Wayland compositor, or a desktop that starts one, nested in Selkies' own.

```tf
module "selkies" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/selkies-project/selkies/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
desktop_environment = "labwc"
wayland = true
}
```

## Network Access

The module contacts the network only when it installs:

- `<release_url>/latest`, to resolve the latest release without GitHub's rate-limited API, unless `selkies_version` is set.
- `<release_url>/download/<version>/selkies-<version>-<platform>`, the package.
- The distribution's package repositories, for Xvfb, PulseAudio, and the package's dependencies.

Once it runs, the browser reaches Selkies through Coder's proxy on the workspace's loopback addresses; no WebRTC, STUN, or TURN server is involved.

## Troubleshooting

The logs are in `~/.coder-modules/selkies-project/selkies/logs/`: `install.log`, `start.log`, and Selkies' own `selkies-session.log`. `coder port-forward <workspace> --tcp 8080:8080` reaches the same desktop at `http://localhost:8080`, which browsers treat as a secure context for the clipboard, gamepads, and the microphone.
168 changes: 168 additions & 0 deletions registry/selkies-project/modules/selkies/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { afterEach, describe, expect, it, setDefaultTimeout } from "bun:test";
import path from "node:path";
import {
execContainer,
readFileContainer,
removeContainer,
runContainer,
runTerraformApply,
runTerraformInit,
type TerraformState,
testRequiredVariables,
writeCoder,
writeFileContainer,
} from "~test";

setDefaultTimeout(120_000);

const IMAGE = "node:22-bookworm-slim";

type Variables = Readonly<{
agent_id: string;
desktop_environment?: string;
port?: number;
install_selkies?: boolean;
}>;

interface Scripts {
install: string;
start: string;
}

const scriptsOf = (state: TerraformState): Scripts => {
const scripts: Partial<Scripts> = {};
for (const resource of state.resources) {
if (resource.type !== "coder_script") {
continue;
}
for (const instance of resource.instances) {
const { display_name: name, script } = instance.attributes as Record<
string,
unknown
>;
if (typeof script !== "string") {
continue;
}
if (name === "Selkies: Install Script") {
scripts.install = script;
} else if (name === "Selkies: Start Script") {
scripts.start = script;
}
}
}
if (!scripts.install || !scripts.start) {
throw new Error("the module must create an install and a start script");
}
return scripts as Scripts;
};

let containers: string[] = [];

afterEach(async () => {
for (const id of containers) {
await removeContainer(id);
}
containers = [];
});

// A workspace with the coder CLI stubbed and, as an image carrying Selkies
// would have them, a launcher and an Xvfb on PATH
const workspace = async (launcher?: string): Promise<string> => {
const id = await runContainer(IMAGE);
containers.push(id);
await writeCoder(id, "#!/bin/sh\nexit 0\n");
if (launcher !== undefined) {
await writeFileContainer(id, "/usr/local/bin/selkies-session", launcher, {
user: "root",
});
await writeFileContainer(id, "/usr/local/bin/Xvfb", "#!/bin/sh\n", {
user: "root",
});
await execContainer(
id,
["chmod", "755", "/usr/local/bin/selkies-session", "/usr/local/bin/Xvfb"],
["--user", "root"],
);
}
return id;
};

const mockLauncher = () =>
Bun.file(
path.join(import.meta.dir, "testdata", "selkies-session-mock.sh"),
).text();

const run = (id: string, script: string) =>
execContainer(id, ["bash", "-c", script]);

describe("selkies", async () => {
await runTerraformInit(import.meta.dir);

testRequiredVariables<Variables>(import.meta.dir, { agent_id: "foo" });

it("installs nothing where the image has Selkies, and starts it behind the app's port", async () => {
const scripts = scriptsOf(
await runTerraformApply<Variables>(import.meta.dir, {
agent_id: "foo",
desktop_environment: "xfce",
}),
);
const id = await workspace(await mockLauncher());

const install = await run(id, scripts.install);
expect(install.exitCode).toBe(0);
expect(install.stdout).toContain(
"Selkies and its display server are installed",
);

const start = await run(id, scripts.start);
expect(start.exitCode).toBe(0);
expect(start.stdout).toContain("Selkies is ready on port 8080");
const args = await readFileContainer(id, "/tmp/selkies-session.args");
expect(args.trim().split("\n")).toEqual([
"--port=8080",
"--enable-basic-auth=false",
"--enable-https=false",
"--session=xfce",
]);

const again = await run(id, scripts.start);
expect(again.exitCode).toBe(0);
expect(again.stdout).toContain("Selkies already answers on port 8080");
});

it("installs nothing when install_selkies is false, and says what is missing", async () => {
const scripts = scriptsOf(
await runTerraformApply<Variables>(import.meta.dir, {
agent_id: "foo",
install_selkies: false,
}),
);
const id = await workspace();

const install = await run(id, scripts.install);
expect(install.exitCode).not.toBe(0);
expect(install.stdout).toContain(
"The workspace lacks selkies-session Xvfb and install_selkies is false",
);
});

it("reports a Selkies that exits before it answers", async () => {
const scripts = scriptsOf(
await runTerraformApply<Variables>(import.meta.dir, {
agent_id: "foo",
port: 8081,
}),
);
const id = await workspace(
"#!/bin/sh\necho 'Xvfb did not come up' >&2\nexit 1\n",
);

// The install script runs first, as Coder orders them, and lays out the module's directory
expect((await run(id, scripts.install)).exitCode).toBe(0);
const start = await run(id, scripts.start);
expect(start.exitCode).not.toBe(0);
expect(start.stdout).toContain("Selkies exited before it answered");
expect(start.stdout).toContain("Xvfb did not come up");
});
});
Loading
Loading