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 bucketInside 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
| File | Purpose | Created by |
|---|---|---|
ssl/server_key.pem | The 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.pem | Served 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 needTo 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.ymlThe 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: 2gYAML 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: ledgerA 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: 4gValues 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.