A self-hosted web app for managing Linux package updates across multiple servers. Connect over SSH, check for updates, and apply maintenance from one browser dashboard.
- Multi-distribution updates: APT, DNF, YUM, Pacman, APK, Flatpak, Snap, deb-get, and custom package managers.
- SSH credential vault: reusable password, key, and OpenSSH certificate credentials encrypted at rest with AES-256-GCM.
- Automatic discovery: package managers, OS metadata, installed package inventory, system info, reboot state, and distribution lifecycle status.
- Repository sources: view each system's APT, DNF/YUM, Pacman, APK, and Flatpak repository files with credentials masked, and optionally edit them through
sudoeditwith a diff review and activity history. - Granular maintenance: refresh, upgrade all, priority-based Upgrade All batches, selected-package upgrades, full upgrades, autoremove, cancellation, and remote reboot.
- Dashboard organization: create collapsible groups, arrange systems for a clear fleet overview, display per-group health badges, and set explicit Upgrade All priorities.
- Per-system controls: hidden systems, system duplication, default Upgrade All preselection, package-manager toggles/config, kept-back and pinned package auto-hide, script overrides, ProxyJump, and host-key trust approval.
- Script customization: inspect built-in SSH command scripts, copy them into editable custom scripts, define parser settings, import/export custom package managers, assign overrides per system, and run pre- and post-upgrade scripts around upgrades on all systems or selected ones.
- Scheduling: cron-based refresh, update, autoremove, reboot (optionally only when required), and notification schedules with scoped systems, cache rules, priority-based Upgrade All waves, and schedule run history.
- Notifications: Email/SMTP, Gotify, MQTT, ntfy.sh, Telegram, and Webhook channels with event filters, system scope, immediate or scheduled delivery, test sends, and encrypted secrets.
- Home Assistant MQTT: app and per-system update entities with discovery, retained state/attributes, rich metadata, images, and optional install commands.
- Authentication: password login with optional TOTP, passkeys/WebAuthn, OpenID Connect SSO, local users with admin or read-only roles, and API tokens with per-token permissions for external integrations.
- Localized UI: Arabic, English, German, French, Hindi, Japanese, Portuguese, Spanish, Russian, and Chinese with browser-language detection.
- Branding: replace the application name, light and dark mode logos, and colors shown in the sidebar, browser tab, sign-in page, and authenticator apps from Settings > Branding. Accent color and neutral tone can be set separately for light and dark mode, with a live preview and a low-contrast warning.
- Operator-friendly UI: light, dark, or system-following theme, activity history, real-time command output, dashboard status summaries with
is:search filters, quick links from dashboard cards to a system's updates, packages, and activity, links to systems by hostname, version/build info, and Docker-ready production packaging.
Caution
Use on trusted networks only. Do not expose Linux Update Dashboard directly to the internet. For remote access, put it behind TLS, a hardened reverse proxy, authentication, and network-level controls such as VPN or firewall rules.
Important
HTTPS is recommended. Plain HTTP works for basic local dashboard use, but browsers restrict secure-context features such as passkeys/WebAuthn and clipboard actions. Behind a reverse proxy, set LUDASH_BASE_URL to the public https://... URL and enable LUDASH_TRUST_PROXY=true.
Prerequisites:
- Node.js 24.22.2 LTS
- pnpm 12.16.1 through Corepack or global install
- SSH access to at least one Linux server
git clone https://github.com/TheDuffman85/linux-update-dashboard.git cd linux-update-dashboard corepack enable corepack prepare [email protected] --activate pnpm install export LUDASH_ENCRYPTION_KEY=$(openssl rand -base64 32) pnpm run dev
The Vite frontend runs on http://localhost:5173 and proxies API calls to the backend on port 3001. The first visit guides you through creating the admin account.
Production from source:
pnpm run build NODE_ENV=production pnpm run start
The production server serves the API and built frontend on port 3001.
Generate an encryption key once and persist it. Changing LUDASH_ENCRYPTION_KEY later makes existing encrypted credentials unreadable.
export LUDASH_ENCRYPTION_KEY=$(openssl rand -base64 32) export LUDASH_BASE_URL=http://localhost:3001 docker run -d \ --name linux-update-dashboard \ -p 3001:3001 \ -e LUDASH_ENCRYPTION_KEY=$LUDASH_ENCRYPTION_KEY \ -e LUDASH_BASE_URL=$LUDASH_BASE_URL \ -v ludash_data:/data \ ghcr.io/theduffman85/linux-update-dashboard:latest
Set LUDASH_BASE_URL to the URL users and integrations actually use. Behind a reverse proxy, set the public HTTPS URL and add LUDASH_TRUST_PROXY=true.
The API applies a Content Security Policy, limits request bodies to 1 MiB, rejects cross-origin browser mutations, and returns sensitive API responses with private, no-store. Command-line clients that do not send browser Origin or Sec-Fetch-Site headers remain supported. Configure HSTS at the TLS-terminating proxy when appropriate; the application does not set it itself.
services: dashboard: image: ghcr.io/theduffman85/linux-update-dashboard:latest container_name: linux-update-dashboard restart: unless-stopped ports: - "3001:3001" volumes: - dashboard_data:/data environment: - LUDASH_ENCRYPTION_KEY=${LUDASH_ENCRYPTION_KEY} - LUDASH_DB_PATH=/data/dashboard.db - LUDASH_BASE_URL=http://localhost:3001 - TZ=Europe/Berlin - NODE_ENV=production # Reverse proxy: # - LUDASH_BASE_URL=https://dashboard.example.com # - LUDASH_TRUST_PROXY=true volumes: dashboard_data:
Docker Secrets are supported by setting LUDASH_ENCRYPTION_KEY_FILE instead of LUDASH_ENCRYPTION_KEY:
services: dashboard: image: ghcr.io/theduffman85/linux-update-dashboard:latest container_name: linux-update-dashboard restart: unless-stopped ports: - "3001:3001" volumes: - dashboard_data:/data environment: - LUDASH_ENCRYPTION_KEY_FILE=/run/secrets/ludash_encryption_key - LUDASH_DB_PATH=/data/dashboard.db - LUDASH_BASE_URL=http://localhost:3001 - NODE_ENV=production secrets: - ludash_encryption_key secrets: ludash_encryption_key: file: ./secrets/ludash_encryption_key.txt volumes: dashboard_data:
Create the secret before starting:
mkdir -p ./secrets
openssl rand -base64 32 > ./secrets/ludash_encryption_key.txt
docker compose up -dFor plain docker run, mount the secret file read-only and set LUDASH_ENCRYPTION_KEY_FILE=/run/secrets/ludash_encryption_key.
The repository also includes docker/docker-compose.yml for local image builds:
cd docker export LUDASH_ENCRYPTION_KEY=$(openssl rand -base64 32) export LUDASH_BASE_URL=http://localhost:3001 export TZ=Europe/Berlin docker compose up -d
The image includes a health check for GET /api/health; loopback requests do not require auth, external requests do. The app returns {"status":"ok"} when healthy.
| Variable | Required | Default | Description |
|---|---|---|---|
LUDASH_ENCRYPTION_KEY |
Yes | - | Secret used for stored credential and notification-secret encryption. Raw 32-byte base64 keys are used directly; other values are derived with PBKDF2-SHA256. |
LUDASH_ENCRYPTION_KEY_FILE |
No | - | Docker Secrets/file alternative for LUDASH_ENCRYPTION_KEY. Do not set both. |
LUDASH_DB_PATH |
No | ./data/dashboard.db |
SQLite database path. Docker images default this to /data/dashboard.db. |
LUDASH_SECRET_KEY |
No | auto-generated | JWT session signing secret, persisted to .secret_key next to the database when omitted. |
LUDASH_SECRET_KEY_FILE |
No | auto-generated | File alternative for LUDASH_SECRET_KEY. Do not set both. |
LUDASH_PORT |
No | 3001 |
HTTP server port. |
LUDASH_HOST |
No | 0.0.0.0 |
HTTP bind address. |
LUDASH_BASE_URL |
Recommended | http://localhost:3001 |
Public URL used for WebAuthn/OIDC and integration URLs such as Home Assistant entity_picture/origin.url. |
LUDASH_TRUST_PROXY |
No | false |
Trust X-Forwarded-* headers behind a reverse proxy. |
TZ |
No | UTC |
IANA timezone for the container/process, UI time display, and cron schedules. |
LUDASH_LOG_LEVEL |
No | info |
debug, info, warn, or error. Debug adds SSH diagnostics and scheduler refresh logs. |
LUDASH_DEFAULT_CACHE_HOURS |
No | 12 |
Startup default for cache reuse hours. User settings can override runtime behavior. |
LUDASH_DEFAULT_SSH_TIMEOUT |
No | 30 |
Startup default SSH connect timeout in seconds. |
LUDASH_DEFAULT_CMD_TIMEOUT |
No | 120 |
Startup default SSH command timeout in seconds. |
LUDASH_MAX_SSH_TIMEOUT |
No | 120 |
Maximum SSH timeout accepted by settings/API. |
LUDASH_MAX_CMD_TIMEOUT |
No | 600 |
Maximum command timeout accepted by settings/API. |
LUDASH_MAX_CONCURRENT_CONNECTIONS |
No | 5 |
Startup default for simultaneous SSH connections. User settings can override runtime behavior. |
LUDASH_MIN_SCHEDULE_INTERVAL_MINUTES |
No | 5 |
Minimum interval accepted for cron schedules. |
VITE_MIN_SCHEDULE_INTERVAL_MINUTES |
No | 5 |
Build-time client hint for schedule interval warnings; set it to match LUDASH_MIN_SCHEDULE_INTERVAL_MINUTES when customizing. |
LUDASH_EOL_CATALOG_FILE |
No | - | Custom distribution lifecycle catalog used by pnpm run generate:eol and pnpm run build. |
NODE_EXTRA_CA_CERTS |
No | - | Additional PEM CA bundle for outbound TLS targets such as OIDC, SMTP, Gotify, ntfy, or webhooks. |
NODE_ENV |
No | - | Set to production to serve the built SPA from the backend. |
For secret-backed settings, do not set both VAR and VAR_FILE; startup fails with a configuration error when both are present for the same setting.
OIDC and password login are normally configured under Settings. Each of the following variables pins one setting from the deployment instead. A pinned setting overrides the saved value, is locked in Settings with a note naming the variable, and is never written to the database. Removing the variable and restarting restores the last value saved in Settings. Unset or empty variables leave the setting managed in Settings, and invalid values stop startup with a configuration error.
| Variable | Setting | Notes |
|---|---|---|
LUDASH_OIDC_ISSUER |
Issuer URL | http:// or https:// URL. Must be set together with LUDASH_OIDC_CLIENT_ID. |
LUDASH_OIDC_CLIENT_ID |
Client ID | Must be set together with LUDASH_OIDC_ISSUER. |
LUDASH_OIDC_CLIENT_SECRET |
Client secret | LUDASH_OIDC_CLIENT_SECRET_FILE is supported for Docker Secrets. Never returned by the API. |
LUDASH_OIDC_SCOPE |
Scopes | Space-separated; must include openid. |
LUDASH_OIDC_GROUPS_CLAIM |
Groups claim | Default groups. |
LUDASH_OIDC_ADMIN_GROUPS |
Admin groups | Comma-separated. |
LUDASH_OIDC_READ_ONLY_GROUPS |
Read-only groups | Comma-separated. |
LUDASH_OIDC_UNMATCHED_ROLE |
Access for unmatched users | deny, admin, or read-only. |
LUDASH_OIDC_END_SESSION |
Sign out of the identity provider on logout | true or false. Default false. |
LUDASH_OIDC_BACKCHANNEL_LOGOUT |
End sessions when signing out at the identity provider | true or false. Default false. |
LUDASH_OIDC_AUTO_LOGIN |
Sign in with SSO automatically | true or false. Default false. Only active while OIDC is configured and password login is disabled. |
LUDASH_OIDC_AUTO_LINK_USERNAME |
Link existing accounts by username | true or false. Default false. See Linking local and SSO sign-in. |
LUDASH_PASSWORD_LOGIN_ENABLED |
Password login | true or false. |
services: linux-update-dashboard: environment: - LUDASH_OIDC_ISSUER=https://auth.example.com/application/o/ludash/ - LUDASH_OIDC_CLIENT_ID=ludash - LUDASH_OIDC_CLIENT_SECRET=${LUDASH_OIDC_CLIENT_SECRET} - LUDASH_OIDC_ADMIN_GROUPS=ludash-admins - LUDASH_PASSWORD_LOGIN_ENABLED=false
Password login only stays disabled, whether from the environment or from Settings, while OIDC is configured (issuer and client ID) or an enabled administrator has a passkey. Otherwise it stays enabled, a warning is logged, and Settings shows a notice. If the identity provider becomes unreachable while password login is disabled, set LUDASH_PASSWORD_LOGIN_ENABLED=true and restart to sign in with a password again; password hashes are kept while password login is disabled.
Linux Update Dashboard supports password login, TOTP authenticator codes, passkeys, OpenID Connect SSO, and API tokens. Passwords use bcrypt, TOTP secrets are encrypted at rest, sessions are HTTP-only JWT cookies with rolling refresh, and password login cannot be disabled unless an administrator has a passkey or SSO is configured, to prevent lockout. OIDC and password login can also be pinned from the deployment; see Authentication settings from the environment.
TOTP can be enabled from Settings > Profile after signing in with the account password. Once enabled, password sign-ins require a current authenticator app code.
For OIDC, configure the provider callback URL as {LUDASH_BASE_URL}/api/auth/oidc/callback. Set LUDASH_BASE_URL before configuring OIDC so callback and origin validation match the public URL.
If the provider cannot be reached at startup or when saving the settings, the dashboard still runs with local sign-in, retries every minute, and turns SSO on without a restart once the provider responds.
OIDC settings also control the requested scopes (must include openid), the claim that carries group membership (default groups, read from the ID token and the UserInfo endpoint), and which groups get admin or read-only access. Admin groups take precedence over read-only groups. Users that match no configured group are denied by default; you can instead grant them admin or read-only access. Installations that already used OIDC before group mapping existed keep granting admin access to unmatched users, so existing SSO accounts are not locked out after upgrading. Role changes apply on the next SSO sign-in, and OIDC sessions expire after one day.
With Sign out of the identity provider on logout enabled under Settings → Authentication → OIDC (SSO), or LUDASH_OIDC_END_SESSION=true, logging out of the dashboard also ends the SSO user's session at the identity provider (OpenID Connect RP-initiated logout), so the next SSO sign-in asks for credentials again. It is off by default because the provider may reject the redirect until the post-logout redirect URI is registered; installations that already used OIDC when upgrading keep it on. This requires the provider to advertise an end_session_endpoint; register {LUDASH_BASE_URL}/login as a post-logout redirect URI with your provider. Leave it off to only clear the dashboard session.
To end dashboard sessions when a user signs out at the identity provider, enable End sessions when signing out at the identity provider (or LUDASH_OIDC_BACKCHANNEL_LOGOUT=true) and register {LUDASH_BASE_URL}/api/auth/oidc/backchannel-logout as the back-channel logout URI with your provider (OpenID Connect Back-Channel Logout). While the setting is off, logout notifications are rejected. The provider must be able to reach the dashboard directly, and logout tokens must be signed with a key from the provider's JWKS or, for HMAC tokens, with the client secret. A logout token with a session ID (sid) ends only the sessions from that provider sign-in, even when it also names the user (sub); if it names the user, it also ends that user's sessions that carry no sid, such as sessions from before the upgrade. Without sid, it ends all dashboard sessions of the named user. Repeated notifications with the same issuer and token ID (jti) are acknowledged without ending later sign-ins.
With Sign in with SSO automatically enabled (or LUDASH_OIDC_AUTO_LOGIN=true), the login page redirects straight to the identity provider. It only applies while OIDC is configured and password login is disabled; otherwise the setting stays inactive, Settings shows a notice, and the server logs why. After a logout, or when an SSO sign-in was denied, the login page is shown instead of redirecting, so users are not signed straight back in. Open {LUDASH_BASE_URL}/login?local=1 to reach the login page anyway, for example to sign in with a passkey.
Read-only users can view the dashboard, systems, available updates, and activity history, but cannot run checks, upgrades, or reboots, change systems, or open credentials, schedules, notifications, or scripts. In Settings they only see Profile. API tokens owned by a read-only user can only read, whatever their permissions.
API tokens are for external integrations such as gethomepage widgets, scripts, monitoring, and Telegram command automation. Every token can read and gets write access only from the permissions you choose: checking for updates, applying updates, organizing systems, removing systems, adding systems, and editing systems. Tokens expire after 30/60/90/365 days or never, are stored as SHA-256 hashes, are limited to 25 per user, and cannot access management endpoints.
Existing tokens are migrated automatically: read-only tokens keep read access only, and read/write tokens get every permission except adding and editing systems. After this migration, the database no longer works with older versions of the app, so back it up before upgrading if you might roll back.
A token that can add or edit systems chooses which host a stored credential connects to, so stored passwords are sent to that host; protect it like an administrator password. See Token permissions for the details and how to add a system through the API.
curl -H "Authorization: Bearer ludash_..." http://localhost:3001/api/dashboard/statsThe endpoint catalog lives in API.md.
Each system's page lives at {LUDASH_BASE_URL}/systems/<id>. To link to systems from another tool, such as a monitoring dashboard or a wiki, without looking up their IDs, open the systems list with one of these parameters:
| Parameter | Effect |
|---|---|
host=<hostname> |
Lists the systems whose hostname or hostname:port matches the value exactly, ignoring case. System names are not matched. |
search=<text> |
Opens the list with that text in the search field, matching the same columns as typing it there. |
redirect=1 |
Together with host or search: opens the system's page directly when exactly one system matches. With no match or several matches, the filtered list opens instead. |
{LUDASH_BASE_URL}/systems?host=web1.example.com
{LUDASH_BASE_URL}/systems?host=web1.example.com:2222&redirect=1
{LUDASH_BASE_URL}/systems?search=debian
Hostnames are not unique: the same host can be added several times, for example with different users for APT and Flatpak. Such links therefore open the list rather than picking one system. If both host and search are given, host is used. Editing the search field removes host and redirect from the address, so the current search can be bookmarked or shared.
Under Settings > Users, administrators create local users with the admin or read-only role, change roles, disable and re-enable accounts, reset passwords, reset TOTP and passkeys, and delete users. A new or reset password can require a change at the next password sign-in; until then the user only sees a change-password page. Disabling, resetting, and deleting end the user's sessions, and a disabled user's API tokens stop working. User management changes are written to the server log.
OIDC users are listed after their first sign-in. Their role comes from the identity provider on every sign-in, so they can only be deleted here; they are created again at their next SSO sign-in.
Accounts with a linked SSO identity are shown as Local + SSO with both roles. They are managed like local accounts, and Remove SSO link removes the identity without touching the local sign-in.
A local account can also sign in with one SSO identity, so a single account has both sign-in methods and local sign-in stays available during an identity provider outage. The strictest setup is still a separate emergency account that is not linked to anything; linking is for when you want one account per person.
To link an account, sign in with your password or a passkey and open Settings → Profile → Single sign-on → Link SSO identity. After you sign in at the identity provider, the dashboard shows the identity it received and asks for your password and, if enabled, your authenticator code. Identities are matched by issuer and subject (iss + sub), never by username or email. An identity the group settings deny cannot be linked.
If the identity already has a separate SSO account, such as admin#oidc-1a2b3c4d5e6f, confirming the link merges it into the local account and removes it. Its API tokens move over and keep working with the same access. Its display preferences are dropped in favor of the local account's. A disabled SSO account is not merged; delete it first if that person should have access again.
What each sign-in method can do on a linked account:
- Password and passkey sessions use the local role set under Settings → Users. Nothing from the identity provider changes it, ends those sessions, or blocks that sign-in. A local username is never renamed.
- SSO sessions use the role from the identity provider, last updated at SSO sign-in. A denied identity loses SSO access only. SSO sessions cannot change the password, TOTP, passkeys or the link, and only see API tokens created through SSO. Those tokens, marked SSO role, are limited by the SSO role instead of the local role.
- Disabling or deleting the account applies to both sign-in methods. So do local credential changes and resets, which end all of the account's sessions.
- Last-administrator checks count either role, but the SSO role only while its provider is still the configured one. Unlinking is refused when the account's SSO role is the last administrator access.
Removing someone from the identity provider does not remove their local password. The dashboard cannot detect this until they try to use SSO, so also disable their account under Settings → Users.
To unlink an account, use the same section with your password and authenticator code, or Remove SSO link as another administrator. This ends the account's sessions, and the next SSO sign-in creates a separate account again. API tokens created through SSO stay valid but follow the local role, and they lose write access if either role was read-only. If the identity provider's issuer changes, existing links stop working; local sign-in keeps working, and each account has to be unlinked and linked again.
Linking existing accounts by username (Settings → Authentication → OIDC (SSO), or LUDASH_OIDC_AUTO_LINK_USERNAME=true) is off by default and meant for migrations. While it is on, an allowed SSO sign-in links automatically to an enabled local account with exactly the same preferred_username, and merges any separate SSO account of that identity, without asking for the local password. Accounts already linked, accounts created while the setting is on, and accounts that were unlinked are left out. These links are logged as warnings and marked Linked by username (unverified).
Usernames from a provider can change and are not proof of ownership, so only turn this on when your provider controls them, and turn it off after migrating. Anyone who gets a matching username at the provider is linked to that account, although they only get their SSO role and none of its local credentials or tokens. Such a mix-up keeps the real owner's identity out until an administrator removes the link.
To prevent lockouts, administrators cannot change, disable, or delete their own account there, at least one enabled administrator must remain, and while passkeys are the only sign-in method an enabled administrator must keep a passkey.
Settings > Profile is available to every user: language, time format, password, TOTP, passkeys, and the SSO link. Language and time format apply only to your own account; the values under General are the defaults for users who have not chosen their own, and the language there is also used for notifications.
The theme is chosen at the bottom of the sidebar: light, dark, or following the operating system, which is the default. The choice is stored in the browser, not in the account.
| Package Manager | Distributions |
|---|---|
| APT | Debian, Ubuntu, Linux Mint |
| DNF | Fedora, RHEL 8+, AlmaLinux, Rocky |
| YUM | CentOS, older RHEL |
| Pacman | Arch Linux, Manjaro |
| APK | Alpine Linux |
| Flatpak | Any cross-distribution host |
| Snap | Any cross-distribution host |
| deb-get | Debian and Ubuntu hosts with deb-get installed |
| Custom | User-defined scripts and parser rules |
Managers are detected over SSH when testing a connection or running the first check. Detected managers are enabled by default and can be toggled per system. Security updates are identified where the package manager exposes enough information.
Per-system manager settings include APT upgrade vs full-upgrade and kept-back auto-hide, DNF upgrade vs distro-sync, optional metadata refresh skips for DNF/Pacman/APK/Flatpak, and opt-in DNF/YUM automation for GPG-key and EULA prompts. Packages pinned or held on the host are marked as kept back instead of being reported as normal updates: APK /etc/apk/world constraints (including packages those constraints hold back), Pacman IgnorePkg/IgnoreGroup, Flatpak masks in the system installation, Snap refresh --hold, and apt-mark hold on deb-get apps. By default these updates move to the system's hidden-updates list, so they do not count as pending; each manager has a setting to keep them in the normal list instead. They return to the normal list once the pin is removed. Pinned packages cannot be selected for individual upgrades, because that would override the pin or do nothing.
APK checks read /etc/apk/world independently of apk upgrade --simulate. Exact name=version pins matching the installed version remain detectable if simulation fails. Other constraints and dependency-based holds require a successful simulation. Failed simulation or world-file reads appear as warnings in live output and check history while available results are retained. Custom APK check scripts need the built-in Reading pinned versions step to use the exact-pin fallback.
Flatpak, Snap, and deb-get checks also warn if mask or hold lookups fail. Available updates are still reported, but pin detection is incomplete until the lookup succeeds; partial output from a failed lookup is discarded. The underlying error is retained in the check output. Existing custom script copies need the updated built-in lookup commands to report these warnings.
While a simulation or lookup is failing, packages that the last successful check found pinned stay marked as kept back, even after a cache refresh. They stay hidden if auto-hide is on, and they cannot be upgraded individually. Pins added or removed in the meantime are picked up once detection succeeds again.
deb-get installs apps that publish .deb files on GitHub, GitLab, or a website but have no APT repository, such as Obsidian or draw.io. The dashboard manages the apps that deb-get installed itself (methods direct, github, gitlab, and website in /etc/deb-get/installed). Apps that deb-get set up from an APT repository or PPA stay with APT.
- Check: runs the read-only
deb-get show <app>for each app and compares versions withdpkg --compare-versions. It does not rundeb-get update, because that also runsapt-get updateand re-checks every app deb-get knows about in the background, which uses up GitHub API limits. - Upgrade all:
deb-get upgrade --dg-only. deb-get also upgrades the apt and ppa apps it installed in this step. - Upgrade selected:
deb-get install <app...>. - No duplicates: deb-get installs its apps through dpkg. While deb-get is enabled, its apps are removed from the APT inventory and from APT updates.
- GitHub token: assign a Secret credential to the built-in
githubTokenentry. deb-get receives it asDEBGET_TOKEN, which raises the GitHub API rate limit. Without it, GitHub-hosted apps can fail to report a published version, and the check output names them. See Secret config entries.
deb-get needs root for every action, including show. The generated sudoers file therefore has an exact deb-get upgrade --dg-only rule, an active deb-get show * rule (see the read-only lookups below), a commented deb-get install * rule for selected upgrades, and env_keep for DEBGET_TOKEN. Any sudo access to deb-get is effectively root: deb-get runs its app definitions as shell code and installs whatever packages they point to.
Use Edit mode on the dashboard to create groups, drag groups into a preferred display order, and drag systems between or within groups. The display order is independent of upgrade execution order.
Each group, including Ungrouped, has an Upgrade priority from 0 to 99 (default 1):
- Lower priority numbers run first.
- Groups with the same priority run in parallel.
- The next priority starts only after all systems at the current priority finish.
- A pre- or post-upgrade script that does not finish cancels the later priorities in that group, so a rolling upgrade does not take down another node.
- Each system also has an Upgrade priority from
0to99within its current group (including Ungrouped), with a default of1. - Lower-priority-number systems run first within that group, while systems with the same priority run in parallel.
- System priority is scoped to its group, so groups sharing a priority progress through their own system priorities independently.
Upgrade All remains available whenever at least one system with updates is idle. Systems that are refreshing, queued, upgrading, rebooting, repairing package-manager issues, or running another maintenance operation are left out until that operation finishes.
Each system has an Upgrade All switch in dashboard Edit mode. Turning it off leaves the system available in the Upgrade All Systems modal but unchecked by default. Checkbox changes made inside the modal apply only to that run and do not change the saved preselection.
The Group badges toggle adds per-group summaries for up-to-date systems, available updates, reboot requirements, lifecycle warnings, check issues, and unreachable systems. The Group upgrade toggle adds an Upgrade button to each group that has systems with updates. Both preferences are stored in the browser and are disabled by default. Groups can be collapsed individually or all at once outside Edit mode without changing their display or upgrade priority.
The dashboard search also accepts status filters: is:uptodate, is:updates, is:reboot, is:os-warning, is:issues, and is:unreachable. Prefix a filter with - to exclude those systems. Clicking a status card at the top of the dashboard applies its filter.
Schedules are managed from the Schedules page. Existing installs migrate to an enabled Default refresh schedule using the previous refresh interval and cache settings.
- Refresh schedules re-check scoped systems when cached results are stale.
- Update schedules refresh scoped systems first, then run the normal per-system Upgrade action where visible updates remain.
- Reboot schedules reboot every scoped system. With Only reboot if required enabled, they refresh scoped systems first and only reboot those that report a pending reboot; systems with a dismissed reboot warning are skipped.
- Notification schedules deliver pending event batches for assigned notification channels.
Schedules use five-field cron expressions in the process timezone. Set Docker TZ, such as TZ=Europe/Berlin, for local-time scheduling. The default minimum interval is 5 minutes and can be changed with LUDASH_MIN_SCHEDULE_INTERVAL_MINUTES.
Update schedules use the same explicit dashboard-group and per-system priorities as manual Upgrade All Systems runs. Hidden systems and systems excluded from Upgrade All are not queued unless explicitly included.
Set a refresh schedule's cache duration to 0 to disable cache reuse. Manual refreshes, server restarts, and newly added systems can still trigger checks outside configured schedules. Notification channels can be assigned to multiple notification schedules.
Notification channels are configured from the Notifications page. Each channel can choose event types (updates, unreachable, appUpdates, and rebootRequired), system scope, immediate or scheduled delivery, and provider-specific settings. rebootRequired fires when a refresh first detects a pending reboot and includes the triggering packages when the host provides /run/reboot-required.pkgs; it can also be included in scheduled digests. New channels default to updates and appUpdates. Secrets such as SMTP passwords, app tokens, bot tokens, command tokens, webhook credentials, and sensitive headers are encrypted at rest.
| Type | Best for | Notes |
|---|---|---|
Email |
inbox alerts | SMTP with optional auth, a customizable subject prefix, Plain SMTP, STARTTLS, or implicit TLS. Prefer NODE_EXTRA_CA_CERTS for private CAs. |
Gotify |
self-hosted/mobile push | App token stored encrypted. |
MQTT |
brokers, automations, Home Assistant | Generic event publishing plus optional Home Assistant MQTT Update entities. |
ntfy |
lightweight push topics | Topic delivery with optional bearer token. |
Telegram |
chat alerts and optional commands | Private-chat binding only. Commands are off by default and require confirmations for mutating actions. |
Webhook |
custom integrations, chat ops, n8n, Node-RED, Discord | POST/PUT/PATCH, templates, query params, headers, auth, retries, timeout, and Discord preset. |
For email channels, set Email subject prefix to a label such as [LINUX-UPDATE] to prepend it to every subject, including test emails and scheduled digests. Leave it blank to keep the original subjects.
LUDASH_BASE_URL should be explicitly set for Home Assistant. The MQTT integration can publish one app update entity and one per-system package update entity. Discovery config uses retained payloads; update state and JSON attributes are published on separate retained topics; install commands map to the normal per-system upgrade action when enabled. Notification schedules only affect the generic MQTT event topic, not Home Assistant state.
Per-system attributes include update counts, security counts, reboot state, reachability, active operation, detected host metadata, and pending package details. Package entity versions are synthetic fingerprints for the pending update set, not real package-version pairs.
Telegram notification setup:
- Create a bot with @BotFather.
- Create a
Telegramchannel in the dashboard and paste the bot token. - Save, reopen the channel, click Create Link, and open the generated private-chat link.
- Start the bot in Telegram and use Send Test to verify delivery.
Bindings use single-use deep links that expire after 10 minutes and show unbound, pending, or bound status. Changing the bot token clears the binding.
Optional commands are private-chat-only, disabled by default, scoped to the channel's systems, and backed by an auto-generated API token that can check for and apply updates. The token is revoked when commands are disabled, the chat is unlinked, the channel is deleted, or the bot token changes. Supported commands are /help, /version, /menu, /status, /refresh <system-id|all>, /packages <system-id>, /upgrade <system-id|all>, /fullupgrade <system-id|all>, and /upgradepkg <system-id> <package>. Mutating commands require confirmation buttons that expire after 5 minutes.
Webhook channels support POST, PUT, and PATCH; custom or Discord presets; no auth, bearer auth, or basic auth; text, JSON template, or form bodies; query params; custom headers; timeout; retries; retry delay; and optional insecure TLS for trusted internal targets.
Templates use simple Mustache variable tags. Only dotted event.* paths are allowed; sections, loops, and other Mustache control tags are rejected. Common variables include {{event.title}}, {{event.body}}, {{event.priority}}, {{event.sentAt}}, {{event.appName}} (the configured branding name), totals under {{event.totals.*}}, text summaries, JSON payload helpers, and JSON-safe variants such as {{event.titleJson}} and {{event.decoratedTitleJson}}.
Webhook delivery defaults to a 10-second timeout, 2 retries, and a 30-second retry delay. URLs must be http or https, embedded URL credentials are rejected, cloud metadata endpoints are blocked, and reserved headers such as Authorization, Host, Content-Length, Connection, and Cookie cannot be set manually.
The Scripts page exposes the SSH command templates for package-manager detection, update checks, installed-package inventory, issue repair, autoremove, upgrades, selected-package upgrades, system info, reboots, and pre- and post-upgrade scripts.
Importable custom package-manager bundles live in examples/, including a Docker Compose example. The Docker Compose example treats services as packages: checks pull images into the local Docker cache and compare them with running containers, while upgrade operations recreate all or selected services. Configure its project path after importing it; the SSH user must be allowed to run Docker. Import the bundle under additional package-manager keys to manage multiple Compose projects on the same system.
- Built-in scripts for APT, DNF, YUM, Pacman, APK, Flatpak, Snap, deb-get, system-info, and reboot are read-only and can be copied. Update checks and installed-package lists for built-in package managers keep the built-in parser, even after you edit the commands of a copy, so the edited commands must print the same format.
- Custom scripts can define shell steps, operation type, parser settings, installed-package inventory parsing, and system-info section mapping.
- Per-system overrides can replace individual operations such as
apt/check_updates,apt/repair_issue,apt/autoremove,apt/upgrade_all, orsystem/reboot. - Custom package managers can define display labels, parser regexes, optional config entries, and import/export bundles.
- Config entries can be marked secret for tokens and passwords. See Secret config entries.
- Pre- and post-upgrade scripts can run commands around an upgrade. See Pre- and post-upgrade scripts.
- Output rules decide from the exit code and output whether a step succeeded, needs attention, or failed. See Output rules.
- Used scripts and package managers are protected from accidental deletion.
Command placeholders include {{package}}, {{packages}}, {{quotedPackage}}, {{quotedPackages}}, {{manager}}, {{config.someKey}}, and {{sudo:COMMAND}}. Custom parser regexes should use named capture groups such as packageName, newVersion, currentVersion, architecture, and repository.
For restricted automation accounts, use a dedicated SSH user and leave the dashboard sudo password unset. The Systems page has a Sudoers setup action that generates a least-privilege /etc/sudoers.d allowlist for the selected system. Review generated files before installing them, keep commands exact, prefer absolute executable paths, leave selected-package wildcard rules commented unless needed, and avoid broad rules such as NOPASSWD: ALL or sudo sh <writable-script>.
Read-only lookups are the one exception to exact, active rules. Some lookups must run as root and take a different argument list every time, such as deb-get show <app>. The generated file lists these in their own active section as <command> *. Only commands on a fixed allowlist in the dashboard qualify, and only in check or inventory steps. A custom script that passes a variable to any other sudo command still gets a warning instead of a rule.
Example validation after installing a generated file:
sudo chmod 440 /etc/sudoers.d/updater-updater sudo visudo -cf /etc/sudoers.d/updater-updater
Package maintenance remains privileged because package scripts run as root. The goal is to limit the dashboard account to the required maintenance commands.
Use these to prepare a system for patching and restore it afterwards, for example to stop a cluster manager and start it again. Create a custom script with the Pre-upgrade or Post-upgrade operation on the Scripts page. A script only runs where you turn it on:
- On every system: star it on the Scripts page to make it the default. It then runs on all systems that have no choice of their own, including ones you add later.
- On one system: under Scripts in the system form, pick the script. Standard follows the default, and None opts the system out, so it stays out even if the default changes.
Each script runs once per upgrade, however many package managers the system has. They wrap Upgrade All, full upgrades, and selected-package upgrades from every source, including schedules, MQTT, Telegram, and the API. Autoremove, repairs, and reboots do not trigger them.
| Situation | Upgrade | Post-upgrade script |
|---|---|---|
| Pre-upgrade script fails, for example by exiting non-zero | Skipped | Does not run, so undo any partial work in the pre-upgrade script |
| Pre-upgrade script is cancelled or interrupted | Skipped | Does not run; the system page offers to run it |
| Upgrade succeeds, fails, or is cancelled | Ran | Runs |
| SSH drops or the host reboots during the upgrade | Reconnects | Runs after reconnecting |
| The host does not come back within 5 minutes | Unknown | Does not run, because the upgrade may still be running; the system page offers to run it |
| The dashboard restarts during an Upgrade All Systems run | Resumes | Runs when the upgrade finishes |
| The dashboard restarts during a script in an Upgrade All Systems run | Not repeated | Attaches to the running step and runs the remaining steps |
| The dashboard restarts during any other upgrade | Continues on the host | Does not run; the system page offers to run it |
| You cancel while the dashboard is reconnecting and the host cannot be reached | Unknown | Does not run, because the upgrade may still be running; the system page offers to run it |
- Failures: a failed pre- or post-upgrade script fails the operation. In an Upgrade All Systems run, it also cancels the later priorities in the same dashboard group. Cancelling a pre-upgrade script does the same once it has started on the host, or when a post-upgrade script is already pending, because the node may still be out of service. Update schedules are different: they upgrade systems one at a time and continue with the next system. A post-upgrade script that did not finish leaves a warning on the system page, with buttons to run it or dismiss the warning. Before you run it, check that no upgrade is still running on the host.
- Steps: each step runs as its own command with its own shell state, and a failed step stops the script.
- Sudo: when the system has a dashboard sudo password, use one
{{sudo:COMMAND}}per step, because the password is delivered once per step. Scripts that apply to the system, including defaults, are included in the Sudoers setup output. - Warnings: a step that an output rule marks as a warning does not stop the script. The upgrade still runs, and its result becomes a warning.
- Environment:
LUDASH_OPERATIONholdsupgrade_all,full_upgrade_all,upgrade_package, ormanual. Post-upgrade scripts also getLUDASH_UPGRADE_RESULTwithsuccess,warning,failed,cancelled, orunknown.warningmeans the upgrade finished but needs attention, for example because an output rule matched, a pre-upgrade script reported a warning, or SSH dropped and the dashboard reconnected. Scripts that check forsuccessshould also acceptwarning.sudousually does not pass these variables on, so read them in the script itself. - Write them to be safe to repeat: running a pending script from the system page repeats it, and a step that was just starting when the dashboard restarted can run again.
By default, exit code 0 means success and anything else means failure. Some package managers do not follow that. A self-compiled OpenWrt image can make apk report 1 error and exit with 1 on every run although each upgrade worked, and other tools print errors while exiting with 0. Output rules let a custom script decide instead.
Open a custom script on the Scripts page, or copy a built-in one, and add rules under Output rules. Each rule has:
| Field | Meaning |
|---|---|
| Step | Limits the rule to one step. Only for operations whose steps run as separate commands. |
| Exit codes | Comma-separated exit codes. Empty matches any exit code. |
| Output matches | A regular expression that must match the output. ^ and $ match at line starts and ends. |
| Output does not match | A regular expression that must not match the output. |
| Result | Success, Warning, or Failed. |
| Message | Optional text for warnings and failures. $<name> inserts a named group from Output matches. |
- A rule needs exit codes, an output pattern, or both. All of its conditions must hold.
- Rules are checked in order and the first match decides. Without a match, the usual exit-code handling applies, so a script without rules behaves exactly as before.
- Rules read the step's combined stdout and stderr as shown in the activity log. Very long output is read from its last 256 KiB.
- Patterns that can take very long to match, such as nested repeats like
(a+)+or backreferences, are rejected. As a safeguard, each pattern may run for at most 100 ms. A pattern that takes longer is skipped, the live output shows a warning, and the next rule is checked. Try the rules applies the same limit and says which rules it skipped. - Try the rules below the list tests them against pasted output without saving.
Where rules apply:
| Operation | Rules see |
|---|---|
| Check updates, installed packages, issue repair, pre- and post-upgrade scripts | Each step on its own. Rules can be limited to a step by its label. |
| Upgrade all, full upgrade, selected-package upgrades, autoremove | The steps run as one command that stops at the first failed step, so rules see the combined output and the final exit code. |
| Detection, system info, reboot | No rules. |
What each result does:
- Success treats the result as successful, even with a non-zero exit code. In update checks it also hides the parser's warning for that step.
- Warning keeps going but flags the result: the step and the operation show a warning, schedules and Upgrade All Systems count it as a warning, and post-upgrade scripts get
LUDASH_UPGRADE_RESULT=warning. - Failed fails the result even with exit code 0. Where steps run separately, a failed step stops the script, as a non-zero exit code would.
- Rules only change how a result is reported. Parsers still read update lists, pins, and installed packages from the output as usual. Results the dashboard has to guess after a reboot, without a real exit code, are never matched.
Example for the apk case above. On a copy of Upgrade all for APK:
| Output matches | Output does not match | Result | Message |
|---|---|---|---|
^(?<count>\d+) errors?; |
^ERROR: |
Warning | apk counted $<count> pre-existing error(s) |
A copy of the APK update check can do the same for its Detecting kept-back packages step with the result Success. Real problems, such as ERROR: unable to select packages, still fail.
Rules are part of the script, so they are included when you export and import custom package-manager bundles.
Mark a config entry as secret when a script needs a token or password, for example a GitHub token that raises API rate limits. A secret entry has no default value. Instead, it names the environment variable the script reads, such as DEBGET_TOKEN. Each system then assigns a Secret credential from the Credentials page.
- The value is stored encrypted, like every other credential. It is never returned by the API, included in bundle exports, or copied anywhere except the systems that reference it.
- At run time the dashboard sends the value over the SSH channel's stdin, ahead of the sudo password. A short preamble reads it into the environment variable. The command text, the process list on the host, the detached script in
/tmp, the activity history, and the command shown in the browser contain only the variable name. - Command output is redacted of the value before it is streamed, stored, or logged. Values shorter than 6 characters are not redacted.
- Scripts reference the variable, for example
"$DEBGET_TOKEN".{{config.<key>}}placeholders are rejected for secret entries. - Enable Keep through sudo on the entry when a root command needs the variable. sudo's default
env_resetdrops it otherwise, so the generated sudoers file addsDefaults:<user> env_keep += "DEBGET_TOKEN". Command rules stay exact, and the value never appears in sudo's command line or auth log. Accounts with full sudo access need this line too. Names that change shell, loader, interpreter, or sudo behavior, such asPATH,LD_*,BASH_ENV,PYTHONPATH, orSUDO_*, are rejected.
The tool that receives the secret decides what happens next. A tool that passes it to curl on the command line makes it visible in the host's process list while that command runs.
The Repository sources action on the Systems page opens a modal with the repository files of the system's active package managers. The files are read over SSH as the SSH user, without sudo, each time the modal is opened or refreshed.
| Manager | Files |
|---|---|
| APT | /etc/apt/sources.list, /etc/apt/sources.list.d/*.list, /etc/apt/sources.list.d/*.sources |
| DNF | /etc/yum.repos.d/*.repo, /etc/distro.repos.d/*.repo, /usr/share/dnf5/repos.d/*.repo (view only) |
| YUM | /etc/yum.repos.d/*.repo |
| Pacman | /etc/pacman.conf, /etc/pacman.d/*mirrorlist |
| APK | /etc/apk/repositories, /etc/apk/repositories.d/*.list |
| Flatpak | /var/lib/flatpak/repo/config, /etc/flatpak/remotes.d/*.flatpakrepo (view only; use flatpak remote-*) |
Credentials in URLs (https://user:pass@...) and password, token, or secret fields are masked before content leaves the server.
Editing is off by default. Turn on Allow repository source editing in the system settings to edit existing files of that system; while it is off, the modal links to the system settings. Read-only users and API tokens cannot edit or change the setting.
- One file at a time: while a file is being edited, the modal can only be closed with Cancel, so an unsaved edit is not lost to Escape or a click outside.
- Review before writing: saving first validates the syntax of one-line APT entries, deb822 stanzas, and INI section headers, then shows a diff of the change against the file currently on the host. The file is written only after you confirm.
- Concurrent changes: if the file changed on the host since it was loaded, the save is rejected and the file must be reloaded.
- How it is written: the new content travels base64-encoded over the SSH channel's stdin. Root writes the file directly. Other users go through
sudoedit, which copies the file to a temporary file owned by the SSH user, lets a throwaway editor script in/tmp/ludash_*replace that copy, and installs it as root.sudoeditrefuses symlinks and keeps the file's owner and mode. The dashboard reads the file back afterwards to confirm that it changed. Nothing is installed on the host. - History: each save is recorded in the activity history with a unified diff.
- Not editable from the dashboard: files with masked credentials (they would be lost), files over 64 KiB, files that are not valid UTF-8, vendor files, and Flatpak remotes. New files cannot be created yet.
For restricted SSH users, each file needs an exact sudoedit rule. When editing is enabled, the Sudoers setup output lists one commented rule per file found on the host:
# updater ALL=(root) NOPASSWD: sudoedit /etc/apt/sources.list.d/debian.sources
Uncomment only the files you want to edit. Write access to repository sources is root-equivalent: a repository decides which packages the next upgrade installs, and package scripts run as root. Only add repositories you trust, and run Check for updates after an edit to confirm the package manager accepts the new configuration.
Upgrade operations and autoremove run through detached remote scripts with nohup, so they continue if SSH disconnects or the dashboard restarts. The dashboard streams output with tail --pid, captures the exit code from a companion file, and marks setup failures as failed instead of falling back to unsafe direct execution. If monitoring disconnects mid-operation, the remote process may still be running and the UI reports that warning.
| Operation | SSH-safe |
|---|---|
| Upgrade all packages | Yes |
| Full/dist upgrade | Yes |
| Upgrade selected packages | Yes |
| Autoremove unused packages/runtimes | Yes |
| Pre- and post-upgrade scripts | Yes |
| Check for updates | No, read-only and retryable |
| Reboot | No, fire-and-forget |
If a sudo password is configured, it is sent only over the live SSH stdin stream to the privileged command. It is never written to files or environment variables. SSH-safe operations are marked with an SSH-safe badge in activity history.
The dashboard records /etc/os-release fields during refreshes and compares them with a bundled lifecycle catalog. Warnings appear on the dashboard, systems list, and system detail page when a release is near EOL, fully EOL, or in reduced support such as Debian LTS after regular Debian Security Support.
Lifecycle warnings are shown alongside patch status. A fully patched system still counts as up to date while its release receives updates, including during reduced support such as Debian LTS. Only a fully EOL release is excluded, because a system with no updates available may simply have no more patches coming.
The warning window is configurable with EOL Warning Window under Settings > General and defaults to 180 days. Supported catalog keys are ubuntu, debian, devuan, fedora, rhel, rocky, almalinux, centos, centos-stream, alpine, and proxmox. Derivatives are matched only by their own ID; a distribution that is merely ID_LIKE=debian shows an unknown lifecycle status instead of being compared against unrelated Debian releases.
Lifecycle data is generated at build time by scripts/generate-distro-lifecycle-data.mjs from https://endoflife.date/api. The generated data lives in server/generated/distro-lifecycle-data.json, with server/default-distro-lifecycle-catalog.json as an offline fallback. pnpm run build runs the generator before compiling and keeps or falls back to local data if the remote API is unavailable.
Use a custom source catalog for repeatable builds:
LUDASH_EOL_CATALOG_FILE=/path/to/my-eol-catalog.json pnpm run generate:eol
# or
LUDASH_EOL_CATALOG_FILE=/path/to/my-eol-catalog.json pnpm run buildCustom catalogs use the same shape as server/default-distro-lifecycle-catalog.json; each entry needs cycle and eol, with optional supportEnd, supportLabel, and finalSupportLabel.
If you edit server/generated/distro-lifecycle-data.json directly, remember that pnpm run build regenerates it. Use LUDASH_EOL_CATALOG_FILE for repeatable custom data.
For container installs, set LUDASH_LOG_LEVEL=debug and inspect logs:
docker logs -f linux-update-dashboard
At info, logs include startup, configuration, warnings, and errors. debug adds attempt-scoped SSH diagnostics and routine scheduler refresh logs. Failed test-connection requests include a debug reference ID you can match against logs.
Logged SSH diagnostics are limited to safe metadata such as host, port, username, auth type, elapsed time, and filtered auth/debug events. Passwords, sudo passwords, private keys, passphrases, tokens, and raw SSH payloads are never logged. Avoid leaving debug logging enabled longer than needed.
If a login banner is sent by the SSH server, Test Connection shows it below the error. The banner is not written to the logs because it can contain one-time links. "Authentication did not complete" means the handshake succeeded but the server held the login open until the timeout. A common cause is Tailscale SSH with an ACL rule set to "action": "check": it ignores the configured credentials and waits for a browser re-authentication that the dashboard cannot perform. Use "action": "accept" for the dashboard host, or disable Tailscale SSH on the target so its regular SSH server answers.
- Credentials and notification secrets are encrypted at rest with AES-256-GCM and per-entry random IVs/auth tags.
- Encryption keys can be raw base64 or passphrases derived via PBKDF2-SHA256 with 480k iterations and a per-instance salt.
- Sessions use HTTP-only, SameSite=Lax JWT cookies signed with HS256.
- State-changing API requests require a per-session CSRF token header.
- API inputs use strict type, format, and range validation.
- Auth endpoints are rate-limited: setup 3/min, login and WebAuthn verify 5/min, and failed bearer token attempts 20/min per IP.
- API tokens are hash-only, limited to chosen permissions, skip CSRF as stateless bearer requests, and are blocked from management endpoints. They can only configure SSH connections with the opt-in add or edit permission.
- Password login uses timing-safe dummy-hash comparisons to reduce username enumeration risk.
- OIDC client secrets are encrypted at rest and can use private/self-signed CAs through
NODE_EXTRA_CA_CERTS. - Per-system mutexes and a semaphore-based SSH connection limit prevent conflicting or runaway SSH operations.
- Client: React, React Router, TanStack Query, Vite, Tailwind CSS in
client/; production output goes todist/client. - Server: Node.js and Hono in
server/; compiled output goes todist/server. - Database: SQLite with Drizzle schema/migrations; startup creates and upgrades schema automatically.
- SSH:
ssh2connection manager, package-manager parsers, command scripts, output streaming, ProxyJump support, and host-key validation. - Runtime services: scheduling, caching, notification delivery, MQTT/Home Assistant state, app update checks, and distribution lifecycle resolution.
├── .github/ # CI/CD workflows and Dependabot
├── assets/ # Logo assets served by the app
├── client/ # React SPA, routes, hooks, components, styles, locales
├── docker/ # Dockerfile, compose, entrypoint, test systems
├── examples/ # Custom package-manager examples
├── screenshots/ # README screenshots and capture scripts
├── scripts/ # Build-time helper scripts
├── server/ # Hono backend, auth, db, routes, services, ssh
├── shared/ # Code used by both client and server (branding theme, API token permissions)
├── tests/ # Vitest client, server, shared, and integration tests
├── run.sh # Local dev/production/test runner
├── reset-dev-branch.sh # Reset dev branch to main
├── vite.config.ts # Vite + Tailwind config
└── package.json
Use the helper:
./run.sh dev # hot reload, backend :3001, client :5173 ./run.sh # build and start production server on :3001 ./run.sh test # production server plus Docker test systems
Or use pnpm directly:
pnpm run dev pnpm run dev:server pnpm run dev:client pnpm test pnpm run check pnpm run screenshots # regenerate the README screenshots; needs Chrome and free ports 3001 and 5173
./run.sh test stops running services, builds and starts 18 Docker SSH fixtures, builds the frontend, and starts the production server on :3001. Add test systems in the dashboard with host.docker.internal or 172.17.0.1 on Linux and the matching SSH port.
Shared credentials:
- User
testuser, passwordtestpass Sudo password:testpassforludash-test-ubuntu-sudoandludash-test-debian-fish-sudo, optional for most othersludash-test-ubuntu-rootalso acceptsroot/testpassfor root-login banner testing
| Container | SSH Port | Package Manager / Fixture | Base |
|---|---|---|---|
ludash-test-ubuntu |
2001 | APT | Ubuntu 24.04 |
ludash-test-fedora |
2002 | DNF | Fedora 41 |
ludash-test-centos7 |
2003 | YUM | CentOS 7 |
ludash-test-archlinux |
2004 | Pacman | Arch Linux |
ludash-test-flatpak |
2005 | Flatpak | Ubuntu 24.04 |
ludash-test-snap |
2006 | Snap | Ubuntu 24.04 |
ludash-test-ubuntu-sudo |
2007 | APT with sudo password | Ubuntu 24.04 |
ludash-test-debian-fish |
2008 | APT with fish shell | Debian 12 |
ludash-test-debian-fish-sudo |
2009 | APT, fish, sudo password | Debian 12 |
ludash-test-alpine |
2010 | APK | Alpine 3.16 |
ludash-test-apt-keptback |
2011 | APT kept-back fixture | Debian 12 |
ludash-test-apt-snap-partial |
2012 | APT + failing Snap fixture | Ubuntu 24.04 |
ludash-test-dnf-gpg-prompt |
2013 | DNF GPG key prompt fixture | Fedora 41 |
ludash-test-dnf-eula-prompt |
2014 | DNF EULA prompt fixture | Fedora 41 |
ludash-test-apt-dpkg-interrupted |
2015 | APT interrupted dpkg fixture | Debian 12 |
ludash-test-ubuntu-root |
2016 | APT root-login/full sudo fixture | Ubuntu 24.04 |
ludash-test-custom-package-managers |
2017 | All custom package-manager examples | Ubuntu 24.04 |
ludash-test-deb-get |
2018 | deb-get stand-in with local .deb apps, least-privilege sudoers |
Debian 12 |
Fixtures pin older package versions from archived or local repositories while current repos stay active, so package-manager checks report deterministic pending updates. Special fixtures cover kept-back packages, partial multi-manager failures, DNF GPG prompts, DNF EULA prompts, interrupted dpkg repair, root-login behavior, and custom package manager examples in examples/.
The custom package-manager container uses local npm/Python repositories and deterministic Docker Compose/Hermes command fixtures. Build it and run every example end to end with:
docker compose -f docker/test-systems/docker-compose.yml up -d --build custom-package-managers LUDASH_RUN_DOCKER_INTEGRATION=1 pnpm vitest run tests/server/custom-package-manager-examples.integration.test.ts
Import the JSON bundles from examples/ on the dashboard's Scripts page. Then add the container as localhost:2017 with testuser / testpass, disable its built-in APT manager, and run a check. Every custom example reports one update from 1.0.0 to its fixture target; Upgrade all installs them through the same persistent SSH workflow used for normal systems. Reset all fixtures to 1.0.0 whenever you want to repeat the test:
docker exec -u testuser ludash-test-custom-package-managers \
/opt/ludash-custom-package-managers/reset-fixtures.shMost test users are restricted through least-privilege sudoers allowlists in docker/test-systems/sudoers/. Selected-package operations use reviewed argument wildcards only where a fixture needs them.
Docker integration tests are opt-in, for example:
LUDASH_RUN_DOCKER_INTEGRATION=1 pnpm vitest run tests/server/apt-sudo-password.integration.test.ts LUDASH_RUN_DOCKER_INTEGRATION=1 pnpm vitest run tests/server/secret-env.integration.test.ts LUDASH_RUN_DOCKER_INTEGRATION=1 pnpm vitest run tests/server/deb-get.integration.test.ts
Reset dev to main and force push:
./reset-dev-branch.sh
Translations live in client/locales/. Browser-language detection applies to the UI. Server-generated text such as notifications and scheduled messages uses the saved language setting; with Browser default, background server text falls back to English because no browser locale is available.
Contributions for corrections, more natural wording, and new languages are welcome. Keep the same keys as client/locales/en.json.
|
|
CrowdSec Web UI A self-hosted web dashboard for CrowdSec to review alerts, manage decisions, configure notifications, and optionally view runtime metrics. |














