SealSkin

Configuration

What the server keeps in /config and /storage, the keys and certificates, and how to edit the YAML by hand.

The server has no database. Everything it knows is a file under two directories, /config for configuration and /storage for user data, both of which are volumes of the container. The paths on this page are the defaults; every one of them can be moved with a setting.

Users, groups, applications, stores, templates, and the cluster/ records are the objects the nodes of a cluster share. On a single server, and on the node of a cluster that holds them, they are the files below. With SEALSKIN_STORE_URL set to a bucket they live there instead, under the same names, and the files are each node's copy: edit the bucket's objects, not the copies.

All configuration files are meant to be edited by hand as well as through the admin panels. The server watches them and reloads a file when it changes on disk, so an edit takes effect without a restart. Its own writes are atomic (a temporary file replaced over the original) and it recognises them, so the watcher only fires for edits made by someone else.

Directory layout

/config
  admin.json                       first-run key file of `admin`, delete after import
  .docker/config.json              this node's registry credentials (registries.md)
  .netrc                           this node's credentials for app store and catalog hosts
  ssl/
    server_key.pem                 RSA key anchoring the end-to-end encryption
    proxy_cert.pem, proxy_key.pem  TLS certificate and key Caddy serves
  .config/sealskin/
    Caddyfile                      rendered from the template at every start
    app_stores.yml                 the catalogues to fetch
    app_stores_cache/              cached copy of each catalogue, and under logos/ the
                                   pictures it names, served to the pages by this node
    installed_apps.yml             installed applications (references + overrides)
    proot_catalogs.yml             PRoot Apps catalogs: the apps each one holds
    app_templates/*.yml            application templates
    autostart_cache/               cached autostart scripts per app
    keys/admins/<name>             administrator public keys
    keys/users/<name>              user settings and public keys
    groups/<name>                  group settings
    branding/branding.yml          the brand the web app wears: name, logo link, accent, wallpaper, links
    branding/logo.<ext>            the uploaded logo and wallpaper (png, jpg, or webp)
    branding/wallpaper.<ext>
    cluster/nodes/<id>.yml         one record per node: key, address, roles, pool, approval
    cluster/pools/<name>.yml       pools of nodes
    cluster/application_keys.yml   the application keys, hashed, the root key among them
    cluster/settings.yml           sign-in settings written for the whole cluster
    cluster/homes/<user>.yml       which node holds each home directory
    cluster/usage/<day>/<id>.yml   session time each node ran, per user
    node/                          this node's own: peer certificate and key, the trust
                                   bundle, the cluster it joined, shared-file sync state,
                                   and the state of its copy of each PRoot Apps catalog
    sso_keys.yml                   this node's web sign-ins
    sessions.yml                   live sessions, rewritten by the server
    public_shares.yml              public share metadata
  root_key                         first-run root key of `admin`, delete after signing in

/storage
  <username>/<home>/               home directories, mounted at /config in sessions
  <username>/_sealskin_shared_files/   mounted at /config/Desktop/files in every persistent session
  sealskin_ephemeral/              cleanroom homes, deleted when the session stops
  sealskin_uploads/<username>/     chunked uploads in progress
  sealskin_public/                 files behind public share links
  sealskin_home_templates/         meta-app home templates
  sealskin_app_icons/              icons uploaded for meta-apps
  sealskin_proot_apps/<id>/        this node's copy of each PRoot Apps catalog, mounted
                                   read-only at /mnt/proot-apps in the sessions assigned to it
  sealskin_shared_store/<user>/    the users' shared files, on the node that holds the
                                   records of a cluster without a bucket

Inside the container these paths are what the server sees. When it starts a session it has to hand Docker host paths, so at start-up it inspects its own container (the one whose host name is the server's own: found by that name or id, else by the name sealskin) and records how each mount maps to the host. The same inspection discovers the externally mapped API and session ports, which are what generated configuration files tell clients to connect to, and the Docker network sessions are attached to. On Kubernetes the server reads its own pod instead and mounts the volume behind a path into the session with a subPath.

Keys and certificates

FilePurposeCreated by
ssl/server_key.pemThe server's RSA private key. Clients hold the public half and use it to verify the server in the handshake and to wrap the session key.The container on first start (4096-bit), or you. The server exits if it is missing.
ssl/proxy_cert.pem, ssl/proxy_key.pemServed by Caddy on the session port.The container on first start, as a self-signed pair; replace with a trusted certificate for Firefox and mobile.
keys/admins/<name>An administrator's public key, PEM only.The server (first run and the Admins panel) or you.
keys/users/<name>A user's settings and public key.The server (Users panel) or you.

To provide your own server key before the first start:

openssl genpkey -algorithm RSA -out config/ssl/server_key.pem -pkeyopt rsa_keygen_bits:4096
openssl rsa -in config/ssl/server_key.pem -pubout    # the public key clients need

To provide your own administrator, place a public key PEM at keys/admins/<name> before the first start; the server then writes no admin.json, and admin signs in with the root key alone.

Session origins

A session opens at /<session id>/ on the server's own origin. Its page is the server's copy of the application's web client, taken out of the image once per image (see the web sign-in and SEALSKIN_WEB_CLIENT_PATH), and the container answers the session's API alone. The copies live under <node state>/web/, one directory per image digest, and go when no installed application runs the image any more.

With SEALSKIN_SESSION_ISOLATION=true, a session of a web sign-in opens on an origin of its own instead, <session id>.<name>, so its pages share no storage, cookies, or service workers with the web app or with other sessions. The certificate has to cover such names and be trusted by the browser, and DNS has to resolve them to the server: *.<server name>, or, as the installer's Duck DNS certificate does, *.<parent name>, which covers the server's own name and its siblings (Duck DNS resolves every name under a domain). The web app probes a random name under each, and under SEALSKIN_SESSION_DOMAIN when it is set, before opening a session, and a session of a web sign-in is then served on its own origin or not at all. A key-file client's session, a collaboration room of one, and the extension's App Laboratory frame are served from the shared origin either way, as they always have been.

admin.json

Written once, on the first start with no administrator present. It is a client configuration file, the same shape the Users panel produces and the connection page's Export Config writes:

{
  "server_endpoint": "sealskin.example.com",
  "api_port": 8000,
  "session_port": 8443,
  "username": "admin",
  "private_key": "-----BEGIN PRIVATE KEY-----\n...",
  "server_public_key": "-----BEGIN PUBLIC KEY-----\n..."
}

server_endpoint is the value of HOST_URL, or the literal string HOST_URL when that variable was not set. The ports are the ones the container was started with, as discovered from Docker, unless HOST_URL names a port (sealskin.example.com:443), which is then both ports, as behind an ingress. Delete the file after importing it: nothing on the server reads it again, and it is the only copy of the private key.

Users, administrators, and groups

A key-file administrator is a file keys/admins/<name> containing a public key. A user is a file keys/users/<name> with two sections:

--- Settings ---
active: true
groups: [staff, render]
admin: false
persistent_storage: true
public_sharing: false
harden_container: false
harden_openbox: false
edit_templates: false
gpu: true
gpu_share: true
home_migration: true
storage_limit: -1
session_limit: -1
session_cpus: -1
session_memory_mb: -1
session_hours: -1
allowance_hours: -1
allowance_period: month
pools: []
pools_denied: []
--- Public Key ---
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----

Missing settings take the defaults shown; an older file's single group is read as the first of groups. The public key block is empty for a user who signs in through the web sign-in alone, and the server adds entries of its own to such a user: auth, the identity provider account the user is bound to, provider_groups, the groups the provider last named, and approved: false for a user the sign-in created while new users are held, which the dashboard's Approve turns true. Remove auth to let another account sign in as the user.

A group is a file groups/<name> holding a YAML mapping of the switches and limits it sets, and nothing for those it leaves alone, plus pools, pools_denied, and sso_groups, the identity provider groups whose members are in it. Signing in describes how the groups of a user combine. Names may contain letters, digits, _, and -. Files starting with . are ignored.

To rotate a user's key, replace the public key block; to disable a user without deleting their storage, set active: false. admin: true makes a user an administrator, as membership in a group that sets it does.

Application stores

app_stores.yml is a list of catalogues:

- name: SealSkin Apps
  url: https://raw.githubusercontent.com/linuxserver/sealskin-apps/refs/heads/master/apps.yml

The first start seeds it with the default store from SEALSKIN_APP_RESOURCE_PATH. Each store's YAML is downloaded into app_stores_cache/ and refreshed by the auto-update job, or from the App Store panel. A store on a host that asks for a sign-in is fetched with the node's netrc file.

App store catalogues

A catalogue is a YAML document with an apps list. Each entry describes an image and what it can do:

apps:
  - id: firefox                       # unique within the store
    name: Firefox
    logo: https://example.com/firefox.png   # fetched by the server, which serves it to the pages
    url: https://github.com/linuxserver/docker-firefox
    provider: docker
    provider_config:
      image: lscr.io/linuxserver/firefox:latest
      port: 3000                      # the Selkies port inside the container
      type: app
      nvidia_support: true
      dri3_support: true
      url_support: true               # may be launched with a URL
      open_support: true              # may be launched with a file
      extensions: [html, htm, pdf]    # file types it is offered for
      autostart: true                 # fetch the image repository's autostart script
      docker_overrides:               # optional extra containers.run() arguments
        shm_size: 2g

YAML anchors are welcome; the default store uses them to share extension groups between apps. When autostart is true the server fetches the autostart (and autostart-wayland) script from the image's source repository and caches it under autostart_cache/, writing it into the session's home directory at launch so the application starts by itself.

PRoot Apps catalogs

proot_catalogs.yml is the list of catalogs, shared by the nodes of a cluster. Each names its apps by remote and name, which is the app's image tag. A remote is the GitHub owner/repo of a proot-apps repository, whose apps are tags of ghcr.io/<owner>/<repo>, or the base URL its metadata/ is served under and the registry of its images, one space apart (Registries and proxies):

- id: 7c1e2b9a-...                  # generated; the catalog's folder on every node
  name: Office
  auto_update: true                 # fetch changed packages on the auto-update interval
  revision: 3                       # bumped by every change; a node whose copy is older syncs
  apps:
    - remote: linuxserver/proot-apps
      name: libreoffice
    - remote: linuxserver/proot-apps
      name: gui
    - remote: myorg/proot-apps
      name: internal-tool
    - remote: https://git.example/raw/it/proot-apps/main registry.example/it/proot-apps
      name: ledger

A node's copy lives under sealskin_proot_apps/<id>/: metadata/metadata.yml and metadata/img/ for the graphical installer, and for each app a folder named after its image reference with / and : as _ (ghcr.io_<owner>_<repo>_<name>/) holding app.tar.gz, the image's layer, and SHALAYER, its digest, exactly as proot-apps localrepo get would write them. Packages are fetched from the registry, through its mirror and with the node's credential for it, for the node's own architecture, and only when their digest changed. Users and groups name a catalog in proot_catalog by its id.

Installed applications

installed_apps.yml stores one record per installed app. A record references the store entry and keeps only what the administrator changed under overrides; the effective app is the store entry deep-merged with those overrides, so store updates apply on the next cache refresh:

- id: 5a8f1c2e-...                  # generated
  source: SealSkin Apps             # store name from app_stores.yml
  source_app_id: firefox            # id inside that store
  app_template: Default
  users: [all]
  groups: []
  auto_update: true
  home_directories: true
  is_meta_app: false
  overrides:                        # only what differs from the store entry
    name: Work Firefox
    provider_config:
      image: lscr.io/linuxserver/firefox:stable
      env:
        - {name: FOO, value: bar}

Meta-apps (from the App Laboratory) add base_app_id and home_template_name, and keep their own name, logo, and autostart scripts under overrides. Records from versions before 0.3.0 were full snapshots; they are migrated on first load, and the file is rewritten once.

Application templates

Each file in app_templates/ is one template:

name: Kiosk
settings:
  SELKIES_UI_SHOW_SIDEBAR: "false"
  HARDEN_DESKTOP: "true"
  DOCKER_MEM_LIMIT: 4g

Values are strings. Keys that are neither DOCKER_* nor KUBERNETES_* become environment variables of the session container; DOCKER_* keys are translated into Docker run options (memory and CPU limits, capabilities, devices, bind mounts, network mode, and so on), and KUBERNETES_POD_TEMPLATE names the PodTemplate laid over a session pod on Kubernetes. The file name is derived from the name (kiosk.yml); renaming the name key renames the file. The set of variables the editor offers is template_schema.yml in the server package, served to clients as data, and a template may contain keys the editor does not know. Keys from before the Selkies variable renames (SELKIES_H264_*, SELKIES_IS_MANUAL_RESOLUTION_MODE, the SELKIES_CLIPBOARD_* booleans) are translated to their current names when the file is loaded.

A directory of read-only default templates can be supplied with SEALSKIN_DEFAULT_APP_TEMPLATES_PATH; when no template exists at all a blank Default is written to app_templates/default.yml.

Sessions and shares

sessions.yml is the server's record of live sessions: container ids, addresses, tokens, storage paths, and, for rooms, the collaboration tokens. It exists so sessions survive a restart. The server rewrites it, skips records it cannot parse, and drops records whose containers no longer exist; there is no reason to edit it.

public_shares.yml maps share ids to their owner, original file name, password hash, and expiry. The files themselves live under /storage/sealskin_public. Expired entries are removed by a background job every ten minutes.

The Caddyfile

Caddyfile is rendered from the template inside the server package at every start, substituting the two ports and the certificate paths, and Caddy is started with it as a child process. It is not meant to be edited; if you need a different proxy configuration, run the server from source with a modified template.

Environment

Everything else is an environment variable. The Settings Reference lists all of them with defaults; the container additionally understands PUID, PGID, TZ, and HOST_URL, and the LinuxServer.io conventions (FILE__ secrets, UMASK, Docker mods) described in the image's README.