Files
sencho/docs/getting-started/configuration.mdx
T
Anso 392bc15d91 feat(git): swap isomorphic-git for native git transport behind clone seam (#1849)
* feat(git): swap isomorphic-git for native git transport behind clone seam

Replace the isomorphic-git engine (HTTP-only, single importer) with the
native git CLI behind the existing withClonedRepo seam, so SSH deploy
keys, ref semantics, and private CAs become reachable in later PRs.

- resolve-before-fetch: ls-remote pins the branch to an immutable SHA,
  then rev-parse verifies the checkout against it; tip races refuse
- hardened spawns: argv arrays only, protocol allowlist (https only),
  neutralized hooks, isolated HOME and all config channels, no prompts
- token reaches git only via a credential helper reading SENCHO_GIT_TOKEN
  from the child env; never argv or URL
- size cap becomes a workspace watchdog (on-disk measure) keeping the
  same knob and breach message; deterministic final gate added
- Windows: pin http.sslBackend=openssl (schannel ignores sslCAInfo) and
  anchor to Git's bundled CA; NODE_EXTRA_CA_CERTS combines with platform
  defaults instead of replacing them
- error classification retargets to exit code + stderr while preserving
  the contractual mappings (AUTH_FAILED maps to 400, never 401;
  unauthenticated refusals mask as REPO_NOT_FOUND)
- runtime image installs git; tests re-pointed at the transport boundary
  plus a new engine suite (classifier corpus, argv hardening, watchdog)

Zero externally visible behavior change except two edge cases: an empty
branch now surfaces BRANCH_NOT_FOUND, and a mid-fetch force push refuses
instead of materializing the moved tip.

* fix(git): unblock CI on linux kill-path test and codeql log warning

Two CI-only findings from the first pipeline run:

- The scripted spawn child in the transport tests lacked the kill method
  that killTree's POSIX fallback reaches when a fake process group does
  not exist; Linux runs crashed inside the timeout tests while Windows
  (taskkill branch) could not reproduce it. Give the fixture the method
  the real ChildProcess always has.
- CodeQL flagged the workspace-removal warning that interpolated the
  NODE_EXTRA_CA_CERTS path (environment-sourced values are treated as
  sensitive at log sinks). Reword the warning to name the variable
  instead of its value; operators know their own environment.

* fix(git): collapse remaining duplicated test setup so the shared helper is used

* fix(git): close watchdog, size-gate, ref-validator, and kill-ordering gaps in native transport

Resolves the release-blocking findings from an independent pre-merge audit
of the native git transport swap:

- A watchdog-triggered kill mid-clone was misclassified as a generic exit
  failure instead of a size breach, because runGit resolves (not rejects)
  when the child is killed via SIGKILL.
- The final on-disk size measurement failed open when it could not be
  read (workspace removed mid-walk, permissions), letting an unmeasured
  clone through as a success. Now fails closed and logs the real cause.
- The ref-name validator was an overly restrictive allow-list that
  rejected valid branch names (leading underscore, non-ASCII, '#').
  Replaced with a deny-list matching real `git check-ref-format --branch`
  semantics, verified against the git binary, including a per-path-segment
  `.lock` check the first pass missed.
- runGit's timeout handler settled as soon as a kill was issued rather
  than confirmed, racing workspace cleanup against a still-alive child
  tree. It now waits for the child's close event, with a bounded fallback
  if termination is never confirmed, and preserves the timeout
  classification if 'error' fires after the kill.
- Windows killTree now also falls back to child.kill() when taskkill
  itself exits non-zero, not just when it fails to spawn.
- Added a real, non-mocked integration test that drives the credential
  helper through the actual git binary against a local HTTPS server with
  Basic Auth checking. It caught a genuine bug the mocked suite could not
  see: the credential.helper config value was quoted in a way that broke
  git's own absolute-path helper detection, failing every authenticated
  clone. Fixed by removing the quotes.
- Migrated a separately developed test file's mocks off the deleted
  isomorphic-git module onto the native transport seam, matching the
  pattern already used elsewhere, after merging with main pulled in that
  feature.

Also updates two stale comments left over from the isomorphic-git era and
adds a git version check to the Docker runtime image smoke tests.

* fix(git): make credential-helper path safe, unify ref length, and fix Windows kill ordering

Addresses three PR 1 correction items from pre-merge audit:

- credential.helper is a shell string, not argv: interpolating the
  helper's workspace-relative path broke authenticated fetches whenever
  the workspace sat under a directory with a space in its name. The
  config value is now a fixed string that names an environment
  variable instead, so no workspace path character can affect how
  git's shell parses it.
- The transport rejected branch names over 200 characters while the
  route accepted up to 256 and real git has no comparable limit.
  REF_MAX_LEN is now a single exported constant shared by the
  transport and both routes.
- On Windows, taskkill runs as a separate process and could still be
  walking a killed process tree after the direct git child reported
  closed, letting the caller delete the workspace early. Kill
  operations are now awaited to completion (bounded by a timeout)
  before a timed-out or size-breached run settles, on both the close
  and error event paths.

Verified against a real authenticated git server inside the built
runtime image: public HTTPS, private HTTPS with a valid PAT, invalid
PAT, a deleted branch, an oversized repository, and the awkward
workspace-path case, including from a workspace path containing
spaces and shell metacharacters.

* fix(git): reap killed helpers and classify curl refusals
2026-08-28 02:00:08 +00:00

366 lines
20 KiB
Plaintext

---
title: Configuration
description: Environment variables, volume mounts, and the 1:1 path rule.
---
Sencho's deployment is configured through environment variables and Docker volume mounts set on the Sencho container itself: there is no config file to edit inside the container. This page covers that deployment layer.
Operational settings (host alerts, data retention, image-update automation, mesh networking, registries, notifications, and more) are configured in the app after first boot, in the Settings Hub. See the [Settings Reference](/reference/settings) for that runtime layer.
## Required environment variables
| Variable | Description |
|----------|-------------|
| `COMPOSE_DIR` | Absolute path to the directory that contains your Compose stacks. Every subdirectory inside becomes a stack in Sencho. Falls back to `/app/compose` if unset, but per the [1:1 path rule](#compose-directory-the-11-path-rule) you should always set this explicitly to match the host path you mount. |
<Note>
**The JWT signing secret is generated automatically.** Sencho creates a secure random signing key during initial setup and stores it in its database. There is no environment variable for it: you cannot set or override it, and you do not need to.
</Note>
### How Sencho organizes your compose directory
When you point `COMPOSE_DIR` at a directory, Sencho expects each stack to live in its own subdirectory. If you create a stack through the UI, Sencho automatically creates a subfolder and places a blank `compose.yaml` inside it. Sencho does not move or "capture" existing files; it simply treats every subdirectory as a separate stack.
<Warning>
**Do not place Sencho's own compose project inside `COMPOSE_DIR`.** Every subdirectory under the managed stack root is discovered as a stack. If Sencho's deployment directory lives there, the dashboard will list itself and generic stack lifecycle actions (deploy, update, stop, down, delete) are blocked for that stack because they would recreate or remove the instance you are using. Keep Sencho's compose project outside `COMPOSE_DIR`, mount `COMPOSE_DIR` read-write for the stacks you manage, and update Sencho through **Fleet → Node Update**.
</Warning>
## Optional environment variables
| Variable | Default | Description |
|----------|---------|-------------|
| `DATA_DIR` | `/app/data` | Directory where Sencho stores its SQLite database, node registry, and cached metrics. |
| `FRONTEND_URL` | *(empty)* | Frontend origin for CORS. Only needed if the UI is served from a different domain than the API. Leave empty for same-origin setups. |
| `NODE_ENV` | `production` | Set automatically in the Docker image. Only change this for local development. |
| `SENCHO_USER` | *(unset)* | When set to a username present inside the container (`sencho` is pre-created for this purpose), the entrypoint drops privileges to that user at startup instead of running as `root`. See [Running as a non-root user](#running-as-a-non-root-user) below. |
| `API_RATE_LIMIT` | `200` | Global API requests per minute per user session. Applies in production only; development uses a fixed higher cap. Authenticated requests are keyed by user ID, unauthenticated by IP. Internal node-to-node traffic bypasses this limit. |
| `API_POLLING_RATE_LIMIT` | `300` | Rate limit for dashboard polling endpoints, in requests per minute. Applies in production only; development uses a fixed higher cap. Raise it for environments with many concurrent browser sessions behind shared NAT. |
| `SENCHO_UPLOAD_DIR` | OS temp dir (`sencho-uploads` subfolder) | Staging directory for file uploads in the Stack file editor. Uploads spool to disk here, outside `COMPOSE_DIR`, before being written to their final location. Set this if your OS temp partition is too small for the uploads you expect. |
## Advanced environment variables
These tune optional subsystems. Most deployments never set them; the defaults are sensible.
| Variable | Default | Description |
|----------|---------|-------------|
| `TRIVY_BIN` | *(unset)* | Path to a host-installed [Trivy](/operations/trivy-setup) binary for image vulnerability scanning. Sencho prefers a managed install under `DATA_DIR/bin/trivy`, then this path, then `trivy` on `PATH`. |
| `TRIVY_CACHE_DIR` | `DATA_DIR/trivy-cache` | Directory where Trivy caches its vulnerability database. Move it onto a different volume if `DATA_DIR` is space-constrained. |
| `SENCHO_MESH_SUBNET` | *(auto)* | CIDR for this node's `sencho_mesh` network. When unset, Sencho picks the first free `/24` from its candidate list or adopts an existing mesh subnet. Set one only to avoid an overlap with another network on the host. See [Sencho Mesh](/features/sencho-mesh). |
| `SENCHO_MESH_RECONCILE_INTERVAL_MS` | `60000` | How often the central instance re-checks proxy-mode mesh tunnels to detect a peer that rebooted. Lower it for faster peer-reboot detection at the cost of more frequent checks. See [Sencho Mesh](/features/sencho-mesh). |
| `SENCHO_MESH_PROXY_TUNNEL_IDLE_MS` | `0` | Idle timeout before a proxy-mode mesh tunnel tears down and reopens on demand. `0` keeps the tunnel open for the life of the connection. See [Sencho Mesh](/features/sencho-mesh). |
| `GITSOURCE_MAX_CLONE_BYTES` | `104857600` | Maximum on-disk size of the temporary workspace for a single [Git Source](/features/git-sources) clone before it is aborted (100 MB). A shallow Compose clone is tiny; raise it only if you track Compose files in a legitimately large repository. |
| `GITSOURCE_MAX_MATERIALIZED_FILES` | `10000` | Maximum number of files the complete-project materializer may write for one Git-managed stack. Crossing it refuses the pull with the count so far. |
| `GITSOURCE_MAX_MATERIALIZED_BYTES` | `536870912` | Maximum total bytes the complete-project materializer may write for one Git-managed stack (512 MB). Crossing it refuses the pull with the count so far. |
| `GITSOURCE_MAX_BUILD_CONTEXT_BYTES` | `268435456` | Maximum size of a single materialized build context after `.dockerignore` filtering (256 MB). A context that exceeds it, including a repository-root context, is refused. |
| `GITSOURCE_MAX_PATH_DEPTH` | `64` | Maximum directory depth for a materialized repository path. Deeper paths are refused. |
| `GITSOURCE_MAX_FILE_BYTES` | `10485760` | Maximum size of a single materialized file (10 MB). Oversized files are refused. |
| `SENCHO_PUBLIC_URL` | *(request host)* | Set on the primary instance. Its externally reachable `http(s)://` URL, no trailing slash, baked into pilot enrollment so remote agents dial the public hostname rather than the address the admin used at setup. |
| `SENCHO_COMPOSE_COMMAND_TIMEOUT_MS` | `1800000` | Hard timeout for a single Compose command (pull, up, down) during deploy and update, in milliseconds (30 minutes). Sencho kills the command and reports failure if it runs longer than this, regardless of whether it is still producing output. Raise it only for very large images or slow storage. |
| `SENCHO_COMPOSE_STALL_TIMEOUT_MS` | `600000` | Idle-output backstop for deploy and update Compose steps (pull and recreate), separate from the hard timeout above. If a step produces no output for this long while still running, Sencho stops it so a hung image pull surfaces a clear failure and the in-app recovery actions instead of spinning. Raise it on slow links or for heavy local image builds. |
| `SENCHO_ZFS_ARCSTATS_PATH` | *(auto)* | Path **inside the container** to the OpenZFS ARC kstat file, for [ZFS ARC-aware host memory](#zfs-arc-aware-host-memory). Sencho checks this path first, then `/host/proc/spl/kstat/zfs/arcstats`, then `/proc/spl/kstat/zfs/arcstats`. Set it only when your ARC stats live at a non-standard path. |
| `SENCHO_PROC_MEMINFO_PATH` | *(auto)* | Path **inside the container** to `/proc/meminfo`, for [VM memory ballooning](#vm-memory-ballooning). Sencho checks this path first, then `/host/proc/meminfo`, then `/proc/meminfo`. Set it only when you need a custom meminfo path. |
Running a remote host as a pilot agent uses four more variables (`SENCHO_MODE`, `SENCHO_PRIMARY_URL`, `SENCHO_ENROLL_TOKEN`, and `SENCHO_PILOT_CA_FILE`), set only on the remote agent container. Sencho bakes them into the enrollment Compose file it generates, so you rarely write them by hand. See [Pilot Agent](/features/pilot-agent) for the full enrollment walkthrough.
## ZFS ARC-aware host memory
On OpenZFS hosts (TrueNAS SCALE, Proxmox, ZFS on Ubuntu or Debian) the ZFS ARC cache can hold a large share of RAM. ARC is reclaimable on demand, but the Linux kernel reports it as unavailable, so a naive reading counts ARC as used memory and can raise false host-memory alerts.
Sencho reads the ARC kstat when it is available and adds the reclaimable portion back into available memory, so the dashboard memory gauge and host RAM alerts reflect real memory pressure. The reclaimable amount is also shown on the dashboard memory tile as a context line. When no ARC stats are readable the behavior is unchanged.
The ARC kstat is usually visible inside the container at `/proc/spl/kstat/zfs/arcstats` with no extra configuration. If your runtime does not expose it, mount it read-only:
```yaml
volumes:
- /proc/spl/kstat/zfs/arcstats:/host/proc/spl/kstat/zfs/arcstats:ro
```
Sencho checks `SENCHO_ZFS_ARCSTATS_PATH`, then `/host/proc/spl/kstat/zfs/arcstats`, then `/proc/spl/kstat/zfs/arcstats`. Set `SENCHO_ZFS_ARCSTATS_PATH` only if your ARC stats live somewhere else inside the container.
## VM memory ballooning
On Linux virtual machines with memory ballooning enabled (TrueNAS/KVM, Proxmox, VMware), the hypervisor can reclaim guest memory through a balloon driver. The reclaimed amount is tracked in `/proc/meminfo` on the `Balloon:` line but standard memory counters do not account for it, so a ballooned VM can appear memory-critical when the guest workload is actually healthy.
Sencho reads the `Balloon:` field from `/proc/meminfo` when it is available. When that data is present, the memory tile's percentage, gauge bar, used/total figures, and the dashboard health banner use the balloon-adjusted values so they agree with each other. The tile also shows how much memory the VM currently retains (`Current VM Memory`), how heavily that retained memory is used (`Current pressure`), and what the hypervisor has reclaimed (`Balloon reclaimable`). Host RAM alerts are deliberately stricter: they keep using the standard working-set percentage, because unlike ZFS ARC, ballooned memory is reclaimed by the hypervisor and the guest cannot get it back on demand, so a hypervisor squeezing a guest must still raise an alert even when the adjusted numbers look healthy. When `/proc/meminfo` is unreadable or the `Balloon:` field is absent, the behavior is unchanged.
`/proc/meminfo` is usually visible inside the container at `/proc/meminfo` with no extra configuration. If your runtime does not expose it, mount it read-only:
```yaml
volumes:
- /proc/meminfo:/host/proc/meminfo:ro
```
Sencho checks `SENCHO_PROC_MEMINFO_PATH`, then `/host/proc/meminfo`, then `/proc/meminfo`. Set `SENCHO_PROC_MEMINFO_PATH` only if your meminfo lives somewhere else inside the container.
## Listen port
Sencho always listens on `1852` inside the container. The port is fixed and is not read from an environment variable. To expose Sencho on a different host port, remap with Docker's `-p` flag (or the `ports:` key in your compose file):
```yaml
ports:
- "8080:1852" # host 8080 to container 1852
```
Behind a reverse proxy you can keep the standard mapping and let the proxy own the public port.
## Container user
Sencho runs as `root` inside the container by default, matching Portainer, Dockge, Komodo, and Yacht. This is required so Sencho can always write to your compose folders, even when a stack container (commonly anything from `linuxserver/*`) has chowned its own bind mount to another UID. Mounting `/var/run/docker.sock` already grants root-equivalent access to the host, so running the Sencho process itself as root does not change the effective privilege boundary.
**What this means for you:**
- Files Sencho creates inside your compose directory (new stacks, edited `compose.yaml`, generated `.env` files) will be owned by `root` on the host. If you edit those files outside Sencho using your own editor, you will need `sudo` or a one-time `chown`.
- The `/app/data` directory is internal to Sencho; its ownership does not matter for host-side tooling.
### Running as a non-root user
If organisational policy, compliance scanners, or a rootless Docker setup requires the container to drop privileges, set `SENCHO_USER=sencho` in the environment. The entrypoint will chown `/app/data` to the sencho user, match the host's Docker socket group, and exec Node under that user.
```yaml
services:
sencho:
image: saelix/sencho:latest
environment:
- COMPOSE_DIR=/opt/compose
- SENCHO_USER=sencho # opt into non-root mode
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /opt/compose:/opt/compose
- ./sencho-data:/app/data
```
<Warning>
Non-root mode can cause filesystem write failures for any stack whose compose folder is owned by a UID other than `sencho`. If you hit "Failed to save stack" or similar errors after opting in, either `chown` the affected stack folder to the sencho user inside the container, or unset `SENCHO_USER` to return to the default (root) mode.
</Warning>
## SSO environment variables
If you use SSO, configure your identity providers via environment variables:
| Variable | Description |
|----------|-------------|
| `SSO_LDAP_ENABLED` | Enable LDAP/AD authentication |
| `SSO_OIDC_GOOGLE_ENABLED` | Enable Google SSO |
| `SSO_OIDC_GITHUB_ENABLED` | Enable GitHub SSO |
| `SSO_OIDC_OKTA_ENABLED` | Enable Okta SSO |
| `SSO_OIDC_CUSTOM_ENABLED` | Enable a custom OIDC provider (Keycloak, Authentik, Authelia, Zitadel, and others) |
| `SSO_CALLBACK_URL` | External base URL for OAuth callbacks (required behind reverse proxy) |
For end-to-end provider setup walkthroughs, see the [SSO Quickstart →](/getting-started/sso-quickstart). For the full feature reference, see [SSO Authentication →](/features/sso).
## Required volume mounts
### Docker socket
Sencho needs access to the Docker daemon to manage containers:
```yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock
```
### Data directory
Sencho's database persists all your settings, nodes, alerts, and metrics history. Mount a named volume or host path so it survives container restarts:
```yaml
volumes:
- ./sencho-data:/app/data
```
<Warning>
Without a persistent data mount, Sencho will lose all configuration (including registered nodes, alerts, and settings) every time the container restarts.
</Warning>
### Compose directory - the 1:1 path rule
<Warning>
This is the most common source of deployment problems. Read carefully.
</Warning>
When Sencho runs `docker compose up`, it does so on your **host machine**. Docker resolves relative volume paths in your Compose files relative to the **host** path of the stack directory, not the path inside the Sencho container.
**The rule:** Mount your Compose directory at the **exact same path** inside the container as it exists on your host.
```yaml
# ✅ Correct - host path matches container path
volumes:
- /home/user/docker:/home/user/docker
environment:
- COMPOSE_DIR=/home/user/docker
```
```yaml
# ❌ Wrong - paths differ, relative volumes will break
volumes:
- /home/user/docker:/app/compose
environment:
- COMPOSE_DIR=/app/compose
```
If you use a simple path like `/opt/compose` on your host, mount it at `/opt/compose` in the container:
```yaml
volumes:
- /opt/compose:/opt/compose
environment:
- COMPOSE_DIR=/opt/compose
```
## Full docker-compose.yml example
```yaml
services:
sencho:
image: saelix/sencho:latest
restart: unless-stopped
ports:
- "1852:1852"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./sencho-data:/app/data
- /opt/compose:/opt/compose # 1:1 path rule
environment:
- COMPOSE_DIR=/opt/compose
- DATA_DIR=/app/data
```
## Optional: global environment file
If your Compose stacks share common variables (e.g. `PUID`, `PGID`, `TZ`), you can pass an `env_file` to the Sencho container so those variables are available in the host environment when `docker compose` runs:
```yaml
services:
sencho:
image: saelix/sencho:latest
env_file:
- /opt/compose/globals.env # shared vars for all stacks
environment:
- COMPOSE_DIR=/opt/compose
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /opt/compose:/opt/compose
- ./sencho-data:/app/data
```
## Reverse proxy setup
Sencho works behind any reverse proxy. The only requirement is that WebSocket connections are forwarded correctly (used for live logs, container terminals, and the host console).
### Nginx
```nginx
server {
listen 80;
server_name sencho.yourdomain.com;
location / {
proxy_pass http://localhost:1852;
proxy_http_version 1.1;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600s;
}
}
```
### Nginx with SSL (Let's Encrypt)
```nginx
server {
listen 80;
server_name sencho.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name sencho.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/sencho.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sencho.yourdomain.com/privkey.pem;
location / {
proxy_pass http://localhost:1852;
proxy_http_version 1.1;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
}
}
```
Use [Certbot](https://certbot.eff.org/) to obtain and auto-renew certificates: `certbot --nginx -d sencho.yourdomain.com`.
### Traefik (Docker labels)
```yaml
labels:
- "traefik.enable=true"
- "traefik.http.routers.sencho.rule=Host(`sencho.yourdomain.com`)"
- "traefik.http.services.sencho.loadbalancer.server.port=1852"
```
### Traefik with SSL (Let's Encrypt)
```yaml
labels:
- "traefik.enable=true"
- "traefik.http.routers.sencho.rule=Host(`sencho.yourdomain.com`)"
- "traefik.http.routers.sencho.entrypoints=websecure"
- "traefik.http.routers.sencho.tls.certresolver=letsencrypt"
- "traefik.http.services.sencho.loadbalancer.server.port=1852"
# HTTP to HTTPS redirect
- "traefik.http.routers.sencho-http.rule=Host(`sencho.yourdomain.com`)"
- "traefik.http.routers.sencho-http.entrypoints=web"
- "traefik.http.routers.sencho-http.middlewares=redirect-to-https"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
```
<Note>
Traefik handles WebSocket upgrades automatically for HTTP/1.1 backends. No extra configuration needed.
</Note>
### Caddy
```
sencho.yourdomain.com {
reverse_proxy localhost:1852
}
```
Caddy automatically obtains and renews SSL certificates via Let's Encrypt. WebSocket connections are forwarded without additional configuration.
## First boot
After starting Sencho, open `http://localhost:1852` in a browser. On a fresh install you land on the **Cold start** card to create the first admin account; every subsequent visit goes to the regular sign-in screen. The [Quickstart](/getting-started/quickstart#first-boot) shows it in detail.
## Where to next
<CardGroup cols={2}>
<Card title="Stack management" icon="layer-group" href="/features/stack-management">
Deploy, edit, restart, update, and roll back stacks from the cockpit.
</Card>
<Card title="Multi-node" icon="sitemap" href="/features/multi-node">
Add a remote Sencho instance and manage it from the same console.
</Card>
<Card title="Self-hosting" icon="server" href="/operations/self-hosting">
Resource recommendations, networking, and Docker socket security.
</Card>
<Card title="SSO Quickstart" icon="key" href="/getting-started/sso-quickstart">
End-to-end provider setup for OIDC, LDAP, and Active Directory.
</Card>
</CardGroup>