Compare commits

...

7 Commits

Author SHA1 Message Date
henrygd
cbe4824ac3 add env var to disable container image update checks (#2371) 2026-09-21 10:49:53 -04:00
henrygd
2c69197d2d fix(install): improve handling of openwrt user account (#2370)
- Allocate unused UID/GIDs instead of using 999
- Detect existing ID collisions and repair missing shadow entries
- Support userdel and deluser during uninstall
- Fix Docker group membership handling
2026-09-20 18:04:07 -04:00
henrygd
97e6f64bdc fix(ui): swap usage chart shown twice in tabs view (#2356) 2026-09-19 13:27:00 -04:00
henrygd
4a5915b141 add SMART to readme features 2026-09-19 12:11:10 -04:00
henrygd
e68372dce4 update readme features 2026-09-19 12:09:26 -04:00
henrygd
c52f3acb94 fix(docker): enable Debian contrib for ZFS utilities in slim NVIDIA image 2026-09-19 12:03:05 -04:00
hank
c09eb8c6df chore(helm): update app version to 0.20.0 (#2355)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-19 11:39:13 -04:00
11 changed files with 147 additions and 50 deletions

View File

@@ -69,6 +69,7 @@ type dockerManager struct {
usingPodman bool // Whether the Docker Engine API is running on Podman
registryClient *http.Client // Client for registry requests; nil uses a client with a 10-second timeout
imageUpdatesDisabled bool // Whether image update checks are disabled by configuration
imageUpdatesMutex sync.RWMutex // Protects imageUpdates, its entries, and imageUpdatesRunning
imageUpdates map[string]*imageUpdateStatus // Shared update status keyed by normalized image reference
imageUpdatesRunning bool // Whether a background image-update batch is in progress
@@ -688,6 +689,8 @@ func newDockerManager(agent *Agent) *dockerManager {
userAgent: "Docker-Client/",
}
dockerImageCheck, _ := utils.GetEnv("DOCKER_IMAGE_CHECK")
// Read container exclusion patterns from environment variable
var excludeContainers []string
if excludeStr, set := utils.GetEnv("EXCLUDE_CONTAINERS"); set && excludeStr != "" {
@@ -711,6 +714,7 @@ func newDockerManager(agent *Agent) *dockerManager {
sem: make(chan struct{}, 5),
apiContainerList: []*container.ApiInfo{},
excludeContainers: excludeContainers,
imageUpdatesDisabled: dockerImageCheck == "false",
// Initialize cache-time-aware tracking structures
lastCpuContainer: make(map[uint16]map[string]uint64),

View File

@@ -31,6 +31,9 @@ func normalizedImageReference(image string) string {
// refreshImageUpdates starts at most one background batch. Neither its network
// work nor its completion is part of the container metrics wait group.
func (dm *dockerManager) refreshImageUpdates(containers []*container.ApiInfo, now time.Time) {
if dm.imageUpdatesDisabled {
return
}
dm.imageUpdatesMutex.Lock()
defer dm.imageUpdatesMutex.Unlock()
if dm.imageUpdatesRunning {

View File

@@ -27,6 +27,29 @@ func waitForImageUpdates(t *testing.T, dm *dockerManager) {
}, time.Second*3, time.Millisecond)
}
func TestDisableDockerImageUpdateCheck(t *testing.T) {
t.Setenv("BESZEL_AGENT_DOCKER_IMAGE_CHECK", "false")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/version" {
fmt.Fprint(w, `{"Version":"25.0.0"}`)
return
}
http.NotFound(w, r)
}))
defer server.Close()
t.Setenv("BESZEL_AGENT_DOCKER_HOST", server.URL)
dm := newDockerManager(nil)
require.True(t, dm.imageUpdatesDisabled)
dm.registryClient = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
t.Fatal("disabled image update check made a registry request")
return nil, nil
})}
dm.refreshImageUpdates([]*container.ApiInfo{{Image: "nginx", Names: []string{"/nginx"}}}, time.Now())
require.False(t, dm.imageUpdatesRunning)
require.Nil(t, dm.imageUpdates)
}
func TestImageUpdateCacheAndStats(t *testing.T) {
local := "sha256:" + strings.Repeat("a", 64)
remote := "sha256:" + strings.Repeat("b", 64)

View File

@@ -70,7 +70,9 @@ RUN set -eux; \
# --------------------------
FROM --platform=$TARGETPLATFORM debian:bookworm-slim AS zfsutils-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
# zfsutils-linux is distributed in Debian's contrib component.
RUN sed -i 's/Components: main/Components: main contrib/' /etc/apt/sources.list.d/debian.sources \
&& apt-get update && apt-get install -y --no-install-recommends \
zfsutils-linux \
&& rm -rf /var/lib/apt/lists/*

View File

@@ -211,7 +211,6 @@ export default memo(function SystemDetail({ id }: { id: string }) {
<TemperatureChart {...coreProps} setPageBottomExtraMargin={setPageBottomExtraMargin} />
<FanChart {...coreProps} />
<BatteryChart system={system} {...coreProps} />
<SwapChart chartData={chartData} grid={grid} dataEmpty={dataEmpty} systemStats={systemStats} />
{pageBottomExtraMargin > 0 && <div style={{ marginBottom: pageBottomExtraMargin }}></div>}
</div>
</TabsContent>

View File

@@ -15,9 +15,10 @@ It has a friendly web interface, simple configuration, and is ready to use out o
- **Lightweight**: Smaller and less resource-intensive than leading solutions.
- **Simple**: Easy setup with little manual configuration required.
- **Alerts**: Configurable alerts for most metrics. Supports many notification services.
- **Docker stats**: Tracks CPU, memory, and network usage history for each container.
- **ZFS**: Tracks pool capacity, health, and I/O, plus per-dataset usage.
- **Alerts**: Configurable alerts for CPU, memory, disk, bandwidth, temperature, fan speed, load average, and status.
- **Network monitoring**: Monitor response time and interruptions directly from agents.
- **S.M.A.R.T.**: Disk health data and notifications on drive failure.
- **Multi-user**: Users manage their own systems. Admins can share systems across users.
- **OAuth / OIDC**: Supports many OAuth2 providers. Password auth can be disabled.
- **Automatic backups**: Save to and restore from disk or S3-compatible storage.
@@ -51,7 +52,7 @@ The [quick start guide](https://beszel.dev/guide/getting-started) and other docu
- **Temperature** - Host system sensors.
- **Fan speed** - Host system sensors (Linux, via `/sys/class/hwmon`).
- **GPU usage / power draw** - Nvidia, AMD, and Intel.
- **Battery** - Host system battery charge.
- **Battery charge** - Host system and some peripherals.
- **Containers** - Status and metrics of all running Docker / Podman containers.
- **S.M.A.R.T.** - Host system disk health (includes eMMC wear/EOL and Linux mdraid array health via sysfs when available).
- **ZFS** - Pool capacity, usage, health, I/O throughput, scrub status, and per-dataset usage.

View File

@@ -2,9 +2,9 @@ apiVersion: v1
description: Installs beszel-agent in kubernetes
home: https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-agent
name: beszel-agent
appVersion: "0.19.0"
appVersion: "0.20.0"
# Bump this version when publishing chart changes.
version: 0.1.6
version: 0.1.7
sources:
- https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-agent
- https://www.beszel.dev/

View File

@@ -80,7 +80,7 @@ Essential parameters to configure:
| `secret.sshKey` | `ssh-key` | Key name in the secret for the SSH public key |
| `secret.tokenKey` | `token` | Key name in the secret for the authentication token |
| `image.repository` | `henrygd/beszel-agent` | Container image |
| `image.tag` | Chart AppVersion (0.19.0) | Image version |
| `image.tag` | Chart AppVersion (0.20.0) | Image version |
| `hostNetwork` | `false` | Use host network for network monitoring |
| `tolerations` | Allows all taints | Tolerations for running on tainted nodes |
@@ -385,7 +385,7 @@ helm upgrade beszel-agent ./beszel-agent \
# Change image version
helm upgrade beszel-agent ./beszel-agent \
--set image.tag="0.19.0"
--set image.tag="0.20.0"
```
### Restart All Agents
@@ -522,7 +522,7 @@ kubectl get secret beszel-agent -o jsonpath='{.data.ssh-key}' | base64 -d
## Chart Information
- **Chart Version**: 0.1.0
- **App Version**: 0.19.0
- **App Version**: 0.20.0
- **Kubernetes Version**: 1.19+
- **Maintainer**: cloudwithdan (nikoloskid@pm.me)

View File

@@ -2,9 +2,9 @@ apiVersion: v1
description: Installs beszel-hub in kubernetes
home: https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-hub
name: beszel-hub
appVersion: "0.19.0"
appVersion: "0.20.0"
# Bump this version when publishing chart changes.
version: 0.1.6
version: 0.1.7
sources:
- https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-hub
- https://www.beszel.dev/

View File

@@ -47,7 +47,7 @@ Key configuration options in `values.yaml`:
|-----------|---------|-------------|
| `replicaCount` | `1` | Number of Beszel Hub replicas |
| `image.repository` | `henrygd/beszel` | Container image repository |
| `image.tag` | Chart AppVersion (0.19.0) | Container image tag |
| `image.tag` | Chart AppVersion (0.20.0) | Container image tag |
| `image.pullPolicy` | `IfNotPresent` | Image pull policy |
| `service.port` | `8090` | Service port |
| `persistentVolumeClaim.enabled` | `true` | Enable persistent volume |
@@ -169,7 +169,7 @@ tolerations:
```yaml
replicaCount: 3
image:
tag: "0.19.0"
tag: "0.20.0"
service:
type: LoadBalancer
ingress:
@@ -330,7 +330,7 @@ By default, Beszel Hub uses a PersistentVolumeClaim for data storage. Ensure you
## Chart Information
- **Chart Version**: 0.1.0
- **App Version**: 0.19.0
- **App Version**: 0.20.0
- **Kubernetes Version**: 1.19+
- **Maintainer**: cloudwithdan (nikoloskid@pm.me)

View File

@@ -298,6 +298,94 @@ warn() {
echo "Warning: $*" >&2
}
# Keep IDs within 16 bits, including on older OpenWrt versions whose account
# helpers start automatic allocation at 65536.
openwrt_unused_id() {
awk -F: '
{ used[$3] = 1 }
END {
for (id = 32768; id < 65534; id++) {
if (!(id in used)) { print id; exit }
}
exit 1
}
' "$1"
}
validate_openwrt_account() {
# Duplicate numeric IDs share permissions even when the names differ.
for account_file in /etc/passwd /etc/group; do
if ! awk -F: '
$1 == "beszel" { id = $3; count++ }
{ ids[$3]++ }
END { if (count && (count != 1 || id == 0 || ids[id] != 1)) exit 1 }
' "$account_file"; then
fail "The beszel account has a duplicate or root ID in $account_file. Stop beszel-agent and assign beszel an unused UID/GID before reinstalling. Update ownership only in $AGENT_DIR; do not change all files owned by the shared ID."
fi
done
if grep -q '^beszel:' /etc/passwd; then
account_gid=$(awk -F: '$1 == "beszel" { print $4 }' /etc/passwd)
group_gid=$(awk -F: '$1 == "beszel" { print $3 }' /etc/group)
[ "$account_gid" = "$group_gid" ] || fail "The beszel user's primary group is not the dedicated beszel group. Repair the account before reinstalling."
fi
}
configure_openwrt_account() (
validate_openwrt_account
[ -r /lib/functions.sh ] || fail "OpenWrt account helpers (/lib/functions.sh) are required."
# OpenWrt's library expects unset variables and defines generic functions.
# Source it in a subshell so neither affects the rest of the installer.
set +u
. /lib/functions.sh
IPKG_INSTROOT=""
if ! grep -q '^beszel:' /etc/group; then
account_gid=$(openwrt_unused_id /etc/group) || fail "No unused service GID available."
group_add beszel "$account_gid" || fail "Could not create the beszel group."
fi
account_gid=$(awk -F: '$1 == "beszel" { print $3 }' /etc/group)
[ -n "$account_gid" ] || fail "The beszel group was not created."
if ! grep -q '^beszel:' /etc/passwd; then
account_uid=$(openwrt_unused_id /etc/passwd) || fail "No unused service UID available."
user_add beszel "$account_uid" "$account_gid" "Beszel agent" /nonexistent /bin/false || fail "Could not create the beszel user."
fi
grep -q '^beszel:' /etc/passwd || fail "The beszel account is incomplete."
validate_openwrt_account
# Previous installers omitted the shadow entry. Repair it with a locked
# password, without rewriting an existing account or changing its IDs.
if ! grep -q '^beszel:' /etc/shadow; then
lock /var/lock/passwd || fail "Could not lock the account database."
shadow_status=0
if ! grep -q '^beszel:' /etc/shadow; then
printf 'beszel:!:0:0:99999:7:::\n' >> /etc/shadow || shadow_status=$?
fi
lock -u /var/lock/passwd || fail "Could not unlock the account database."
[ "$shadow_status" -eq 0 ] || fail "Could not create the beszel shadow entry."
fi
if grep -q '^docker:' /etc/group; then
# Match complete member names and avoid a leading comma for empty groups.
if ! awk -F: '$1 == "docker" { n = split($4, members, ","); for (i = 1; i <= n; i++) if (members[i] == "beszel") found = 1 } END { exit !found }' /etc/group; then
echo "Adding beszel to docker group"
if grep -q '^docker:[^:]*:[^:]*:$' /etc/group; then
sed -i '/^docker:/s/$/beszel/' /etc/group
else
sed -i '/^docker:/s/$/,beszel/' /etc/group
fi
fi
fi
)
remove_openwrt_account() {
if command -v userdel >/dev/null 2>&1; then
userdel beszel || fail "Could not remove the beszel user."
elif command -v deluser >/dev/null 2>&1; then
deluser beszel || fail "Could not remove the beszel user."
else
warn "Neither userdel nor deluser is available; the beszel account has been retained."
fi
}
require_value() {
[ "$#" -ge 2 ] && [ -n "$2" ] || fail "Option $1 requires a value."
}
@@ -672,7 +760,9 @@ if [ "$UNINSTALL" = true ]; then
echo "Removing the dedicated user for the agent service..."
killall beszel-agent 2>/dev/null || true # Usually already stopped by the service manager.
if id -u beszel >/dev/null 2>&1; then
if is_alpine || is_openwrt; then
if is_openwrt; then
remove_openwrt_account
elif is_alpine; then
deluser beszel || fail "Could not remove the beszel user."
elif is_freebsd; then
pw user del beszel || fail "Could not remove the beszel user."
@@ -772,32 +862,7 @@ if is_alpine; then
fi
elif is_openwrt; then
# Create beszel group first if it doesn't exist (check /etc/group directly)
if ! grep -q "^beszel:" /etc/group >/dev/null 2>&1; then
echo "beszel:x:999:" >> /etc/group
fi
# Create beszel user if it doesn't exist (double-check to prevent duplicates)
if ! id -u beszel >/dev/null 2>&1 && ! grep -q "^beszel:" /etc/passwd >/dev/null 2>&1; then
echo "beszel:x:999:999::/nonexistent:/bin/false" >> /etc/passwd
fi
# Add the user to the docker group if docker group exists and user is not already in it
if grep -q "^docker:" /etc/group >/dev/null 2>&1; then
echo "Adding beszel to docker group"
# Check if beszel is already in docker group
if ! grep "^docker:" /etc/group | grep -q "beszel"; then
# Add beszel to docker group by modifying /etc/group
# Handle both cases: group with existing members and group without members
if grep "^docker:" /etc/group | grep -q ":.*:.*$"; then
# Group has existing members, append with comma
sed -i 's/^docker:\([^:]*:[^:]*:\)\(.*\)$/docker:\1\2,beszel/' /etc/group
else
# Group has no members, just append
sed -i 's/^docker:\([^:]*:[^:]*:\)$/docker:\1beszel/' /etc/group
fi
fi
fi
configure_openwrt_account
elif is_freebsd; then
if is_opnsense || is_pfsense; then