Compare commits

..

22 Commits

Author SHA1 Message Date
Henry Dollman
26dbb1968a version 0.1.0 2024-07-24 15:34:37 -04:00
Henry Dollman
ee57e84cb8 fallback prompt for copy button in insecure contexts 2024-07-24 15:32:20 -04:00
Henry Dollman
345dbeb757 0.0.1 2024-07-24 10:53:49 -04:00
Henry Dollman
29f5d3ae62 update readme 2024-07-24 10:52:55 -04:00
Henry Dollman
d4b0887153 update forgot password cli instructions 2024-07-24 10:32:25 -04:00
Henry Dollman
06e4dd10e0 0.0.1-alpha.9 2024-07-23 22:41:33 -04:00
Henry Dollman
af4d5137d6 lower 55 sec system update check to 50 sec 2024-07-23 22:41:05 -04:00
Henry Dollman
5e255f8f69 use semaphore to limit concurrency in agent
subtract mem cache from container stats
2024-07-23 22:40:39 -04:00
Henry Dollman
76cfaaa179 0.0.1-alpha.8 2024-07-23 20:09:50 -04:00
Henry Dollman
b89bec31b5 add check for no records 2024-07-23 20:09:09 -04:00
Henry Dollman
0355d9c654 refactoring agent 2024-07-23 20:07:46 -04:00
Henry Dollman
41df7b7392 update agent to use short container id 2024-07-23 19:51:35 -04:00
Henry Dollman
52c77dd361 readme / site style updates 2024-07-23 18:51:49 -04:00
Henry Dollman
ae0f5c938f 0.0.1-alpha.7 2024-07-23 15:47:45 -04:00
Henry Dollman
78dc269538 update logic for batch updating servers 2024-07-23 15:47:15 -04:00
Henry Dollman
f6967eab35 update gitignore / readme 2024-07-23 15:43:26 -04:00
Henry Dollman
e787b6ea1b update docker compose to make docker sock read only 2024-07-23 15:21:03 -04:00
Henry Dollman
844b95dfd0 get container stats synchronously 2024-07-23 15:19:04 -04:00
Henry Dollman
c5776541a0 style / chart axis updates 2024-07-23 14:48:33 -04:00
Henry Dollman
5ba7568acf update readme 2024-07-22 19:01:05 -04:00
Henry Dollman
14c7e2db8f 0.0.1-alpha.6 2024-07-22 16:52:11 -04:00
Henry Dollman
51ed130b53 update bun install github action to not save lockfile 2024-07-22 16:26:08 -04:00
18 changed files with 217 additions and 153 deletions

View File

@@ -30,7 +30,7 @@ jobs:
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --cwd ./hub/site
run: bun install --no-save --cwd ./hub/site
- name: Build site
run: bun run --cwd ./hub/site build

View File

@@ -21,7 +21,7 @@ jobs:
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --cwd ./hub/site
run: bun install --no-save --cwd ./hub/site
- name: Build site
run: bun run --cwd ./hub/site build

1
.gitignore vendored
View File

@@ -6,4 +6,5 @@ temp
beszel
beszel-agent
beszel_data
beszel_data*
dist

View File

@@ -5,7 +5,7 @@ services:
restart: unless-stopped
network_mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
PORT: 45876
KEY: 'ssh-ed25519 YOUR_PUBLIC_KEY'

View File

@@ -2,7 +2,6 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
@@ -23,11 +22,21 @@ import (
psutilNet "github.com/shirou/gopsutil/v4/net"
)
var Version = "0.0.1-alpha.5"
var Version = "0.1.0"
var containerCpuMap = make(map[string][2]uint64)
var containerCpuMutex = &sync.Mutex{}
var sem = make(chan struct{}, 15)
func acquireSemaphore() {
sem <- struct{}{}
}
func releaseSemaphore() {
<-sem
}
var diskIoStats = DiskIoStats{
Read: 0,
Write: 0,
@@ -44,59 +53,20 @@ var netIoStats = NetIoStats{
// client for docker engine api
var client = &http.Client{
Timeout: time.Second * 5,
Timeout: time.Second,
Transport: &http.Transport{
Dial: func(proto, addr string) (net.Conn, error) {
return net.Dial("unix", "/var/run/docker.sock")
},
ForceAttemptHTTP2: false,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
DisableCompression: true,
MaxIdleConnsPerHost: 50,
DisableKeepAlives: false,
},
}
type SystemData struct {
Stats SystemStats `json:"stats"`
Info SystemInfo `json:"info"`
Containers []ContainerStats `json:"container"`
}
type SystemInfo struct {
Cores int `json:"c"`
Threads int `json:"t"`
CpuModel string `json:"m"`
// Os string `json:"o"`
Uptime uint64 `json:"u"`
Cpu float64 `json:"cpu"`
MemPct float64 `json:"mp"`
DiskPct float64 `json:"dp"`
}
type SystemStats struct {
Cpu float64 `json:"cpu"`
Mem float64 `json:"m"`
MemUsed float64 `json:"mu"`
MemPct float64 `json:"mp"`
MemBuffCache float64 `json:"mb"`
Disk float64 `json:"d"`
DiskUsed float64 `json:"du"`
DiskPct float64 `json:"dp"`
DiskRead float64 `json:"dr"`
DiskWrite float64 `json:"dw"`
NetworkSent float64 `json:"ns"`
NetworkRecv float64 `json:"nr"`
}
type ContainerStats struct {
Name string `json:"n"`
Cpu float64 `json:"c"`
Mem float64 `json:"m"`
// MemPct float64 `json:"mp"`
}
func getSystemStats() (SystemInfo, SystemStats) {
func getSystemStats() (*SystemInfo, *SystemStats) {
c, _ := cpu.Percent(0, false)
v, _ := mem.VirtualMemory()
d, _ := disk.Usage("/")
@@ -105,7 +75,7 @@ func getSystemStats() (SystemInfo, SystemStats) {
memPct := twoDecimals(v.UsedPercent)
diskPct := twoDecimals(d.UsedPercent)
systemStats := SystemStats{
systemStats := &SystemStats{
Cpu: cpuPct,
Mem: bytesToGigabytes(v.Total),
MemUsed: bytesToGigabytes(v.Used),
@@ -116,7 +86,7 @@ func getSystemStats() (SystemInfo, SystemStats) {
DiskPct: diskPct,
}
systemInfo := SystemInfo{
systemInfo := &SystemInfo{
Cpu: cpuPct,
MemPct: memPct,
DiskPct: diskPct,
@@ -182,54 +152,63 @@ func getSystemStats() (SystemInfo, SystemStats) {
}
func getDockerStats() ([]ContainerStats, error) {
func getDockerStats() ([]*ContainerStats, error) {
resp, err := client.Get("http://localhost/containers/json")
if err != nil {
return []ContainerStats{}, err
return []*ContainerStats{}, err
}
defer resp.Body.Close()
var containers []Container
var containers []*Container
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
panic(err)
}
containerStats := make([]*ContainerStats, 0, len(containers))
// store valid ids to clean up old container ids from map
validIds := make(map[string]struct{}, len(containers))
var wg sync.WaitGroup
var containerStats []ContainerStats
for _, ctr := range containers {
ctr.IdShort = ctr.ID[:12]
validIds[ctr.IdShort] = struct{}{}
wg.Add(1)
go func() {
defer wg.Done()
cstats, err := getContainerStats(ctr)
if err != nil {
log.Printf("Error getting container stats: %+v\n", err)
return
// retry once
cstats, err = getContainerStats(ctr)
if err != nil {
log.Printf("Error getting container stats: %+v\n", err)
return
}
}
containerStats = append(containerStats, cstats)
}()
}
// clean up old containers from map
validNames := make(map[string]struct{}, len(containers))
for _, ctr := range containers {
validNames[ctr.Names[0][1:]] = struct{}{}
}
for name := range containerCpuMap {
if _, exists := validNames[name]; !exists {
delete(containerCpuMap, name)
wg.Wait()
for id := range containerCpuMap {
if _, exists := validIds[id]; !exists {
// log.Printf("Removing container cpu map entry: %+v\n", id)
delete(containerCpuMap, id)
}
}
wg.Wait()
return containerStats, nil
}
func getContainerStats(ctr Container) (ContainerStats, error) {
resp, err := client.Get("http://localhost/containers/" + ctr.ID + "/stats?stream=0&one-shot=1")
func getContainerStats(ctr *Container) (*ContainerStats, error) {
// use semaphore to limit concurrency
acquireSemaphore()
defer releaseSemaphore()
resp, err := client.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1")
if err != nil {
return ContainerStats{}, err
return &ContainerStats{}, err
}
defer resp.Body.Close()
@@ -240,26 +219,30 @@ func getContainerStats(ctr Container) (ContainerStats, error) {
name := ctr.Names[0][1:]
// memory
usedMemory := statsJson.MemoryStats.Usage - statsJson.MemoryStats.Cache
// memory (https://docs.docker.com/reference/cli/docker/container/stats/)
memCache := statsJson.MemoryStats.Stats["inactive_file"]
if memCache == 0 {
memCache = statsJson.MemoryStats.Stats["cache"]
}
usedMemory := statsJson.MemoryStats.Usage - memCache
// pctMemory := float64(usedMemory) / float64(statsJson.MemoryStats.Limit) * 100
// cpu
// add default values to containerCpu if it doesn't exist
containerCpuMutex.Lock()
defer containerCpuMutex.Unlock()
if _, ok := containerCpuMap[name]; !ok {
containerCpuMap[name] = [2]uint64{0, 0}
if _, ok := containerCpuMap[ctr.IdShort]; !ok {
containerCpuMap[ctr.IdShort] = [2]uint64{0, 0}
}
cpuDelta := statsJson.CPUStats.CPUUsage.TotalUsage - containerCpuMap[name][0]
systemDelta := statsJson.CPUStats.SystemUsage - containerCpuMap[name][1]
cpuDelta := statsJson.CPUStats.CPUUsage.TotalUsage - containerCpuMap[ctr.IdShort][0]
systemDelta := statsJson.CPUStats.SystemUsage - containerCpuMap[ctr.IdShort][1]
cpuPct := float64(cpuDelta) / float64(systemDelta) * 100
if cpuPct > 100 {
return ContainerStats{}, errors.New("cpu pct is greater than 100")
return &ContainerStats{}, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct)
}
containerCpuMap[name] = [2]uint64{statsJson.CPUStats.CPUUsage.TotalUsage, statsJson.CPUStats.SystemUsage}
containerCpuMap[ctr.IdShort] = [2]uint64{statsJson.CPUStats.CPUUsage.TotalUsage, statsJson.CPUStats.SystemUsage}
cStats := ContainerStats{
cStats := &ContainerStats{
Name: name,
Cpu: twoDecimals(cpuPct),
Mem: bytesToMegabytes(float64(usedMemory)),
@@ -268,12 +251,12 @@ func getContainerStats(ctr Container) (ContainerStats, error) {
return cStats, nil
}
func gatherStats() SystemData {
func gatherStats() *SystemData {
systemInfo, systemStats := getSystemStats()
stats := SystemData{
stats := &SystemData{
Stats: systemStats,
Info: systemInfo,
Containers: []ContainerStats{},
Containers: []*ContainerStats{},
}
containerStats, err := getDockerStats()
if err == nil {

View File

@@ -2,8 +2,48 @@ package main
import "time"
type SystemData struct {
Stats *SystemStats `json:"stats"`
Info *SystemInfo `json:"info"`
Containers []*ContainerStats `json:"container"`
}
type SystemInfo struct {
Cores int `json:"c"`
Threads int `json:"t"`
CpuModel string `json:"m"`
// Os string `json:"o"`
Uptime uint64 `json:"u"`
Cpu float64 `json:"cpu"`
MemPct float64 `json:"mp"`
DiskPct float64 `json:"dp"`
}
type SystemStats struct {
Cpu float64 `json:"cpu"`
Mem float64 `json:"m"`
MemUsed float64 `json:"mu"`
MemPct float64 `json:"mp"`
MemBuffCache float64 `json:"mb"`
Disk float64 `json:"d"`
DiskUsed float64 `json:"du"`
DiskPct float64 `json:"dp"`
DiskRead float64 `json:"dr"`
DiskWrite float64 `json:"dw"`
NetworkSent float64 `json:"ns"`
NetworkRecv float64 `json:"nr"`
}
type ContainerStats struct {
Name string `json:"n"`
Cpu float64 `json:"c"`
Mem float64 `json:"m"`
// MemPct float64 `json:"mp"`
}
type Container struct {
ID string `json:"Id"`
IdShort string
Names []string
Image string
ImageID string

View File

@@ -31,4 +31,4 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8090
ENTRYPOINT [ "/beszel" ]
CMD ["serve", "--http=localhost:8090"]
CMD ["serve", "--http=0.0.0.0:8090"]

View File

@@ -15,24 +15,26 @@ import (
"net/url"
"os"
"strings"
"sync"
"time"
"github.com/labstack/echo/v5"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/plugins/migratecmd"
"github.com/pocketbase/pocketbase/tools/cron"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
)
var Version = "0.0.1-alpha.5"
var Version = "0.1.0"
var app *pocketbase.PocketBase
var serverConnections = make(map[string]Server)
var serverConnections = make(map[string]*Server)
var serverConnectionsLock = sync.Mutex{}
func main() {
app = pocketbase.NewWithConfig(pocketbase.Config{
@@ -207,49 +209,53 @@ func startSystemUpdateTicker() {
}
func updateSystems() {
// handle max of 1/3 + 1 servers at a time
numServers := len(serverConnections)/3 + 1
// find systems that are not paused and updated more than 58 seconds ago
fiftyEightSecondsAgo := time.Now().UTC().Add(-58 * time.Second).Format("2006-01-02 15:04:05")
records, err := app.Dao().FindRecordsByFilter(
"2hz5ncl8tizk5nx", // collection
"status != 'paused' && updated < {:updated}", // filter
"updated", // sort
numServers, // limit
0, // offset
dbx.Params{"updated": fiftyEightSecondsAgo},
"2hz5ncl8tizk5nx", // collection
"status != 'paused'", // filter
"updated", // sort
-1, // limit
0, // offset
)
if err != nil {
app.Logger().Error("Failed to query systems: ", "err", err.Error())
// log.Println("records", len(records))
if err != nil || len(records) == 0 {
// app.Logger().Error("Failed to query systems")
return
}
for _, record := range records {
updateSystem(record)
fiftySecondsAgo := time.Now().UTC().Add(-50 * time.Second)
batchSize := len(records)/4 + 1
for i := 0; i < batchSize; i++ {
if records[i].Get("updated").(types.DateTime).Time().After(fiftySecondsAgo) {
break
}
// log.Println("updating", records[i].Get(("name")))
go updateSystem(records[i])
}
}
func updateSystem(record *models.Record) {
var server Server
var server *Server
// check if server connection data exists
if _, ok := serverConnections[record.Id]; ok {
server = serverConnections[record.Id]
} else {
// create server connection struct
server = Server{
server = &Server{
Host: record.Get("host").(string),
Port: record.Get("port").(string),
}
client, err := getServerConnection(&server)
client, err := getServerConnection(server)
if err != nil {
app.Logger().Error("Failed to connect:", "err", err.Error(), "server", server.Host, "port", server.Port)
updateServerStatus(record, "down")
return
}
server.Client = client
serverConnectionsLock.Lock()
serverConnections[record.Id] = server
serverConnectionsLock.Unlock()
}
// get server stats from agent
systemData, err := requestJson(&server)
systemData, err := requestJson(server)
if err != nil {
if err.Error() == "retry" {
// if previous connection was closed, try again
@@ -310,6 +316,8 @@ func deleteServerConnection(record *models.Record) {
if serverConnections[record.Id].Client != nil {
serverConnections[record.Id].Client.Close()
}
serverConnectionsLock.Lock()
defer serverConnectionsLock.Unlock()
delete(serverConnections, record.Id)
}
}

View File

@@ -32,7 +32,7 @@ export function AddSystemButton() {
restart: unless-stopped
network_mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
PORT: ${port}
KEY: "${publicKey}"

View File

@@ -47,7 +47,13 @@ export default function BandwidthChart({
<YAxis
className="tracking-tighter"
width={75}
domain={[0, (max: number) => (max < 0.4 ? 0.4 : Math.ceil(max))]}
domain={[0, (max: number) => (max <= 0.4 ? 0.4 : Math.ceil(max))]}
tickFormatter={(value) => {
if (value >= 100) {
return value.toFixed(0)
}
return value.toFixed((value * 100) % 1 === 0 ? 1 : 2)
}}
tickLine={false}
axisLine={false}
unit={' MB/s'}
@@ -66,7 +72,8 @@ export default function BandwidthChart({
tickFormatter={hourWithMinutes}
/>
<ChartTooltip
// cursor={false}
animationEasing="ease-out"
animationDuration={150}
content={
<ChartTooltipContent
unit=" MB/s"

View File

@@ -69,7 +69,7 @@ export default function ContainerCpuChart({
>
<CartesianGrid vertical={false} />
<YAxis
domain={[0, (max: number) => Math.max(Math.ceil(max), 0.4)]}
// domain={[0, (max: number) => Math.max(Math.ceil(max), 0.4)]}
width={47}
tickLine={false}
axisLine={false}

View File

@@ -77,7 +77,8 @@ export default function DiskChart({
tickFormatter={hourWithMinutes}
/>
<ChartTooltip
// cursor={false}
animationEasing="ease-out"
animationDuration={150}
content={
<ChartTooltipContent
unit=" GB"

View File

@@ -47,7 +47,7 @@ export default function DiskIoChart({
<YAxis
className="tracking-tighter"
width={75}
domain={[0, (max: number) => (max < 0.4 ? 0.4 : Math.ceil(max))]}
domain={[0, (max: number) => (max <= 0.4 ? 0.4 : Math.ceil(max))]}
tickFormatter={(value) => {
if (value >= 100) {
return value.toFixed(0)
@@ -72,7 +72,8 @@ export default function DiskIoChart({
tickFormatter={hourWithMinutes}
/>
<ChartTooltip
// cursor={false}
animationEasing="ease-out"
animationDuration={150}
content={
<ChartTooltipContent
unit=" MB/s"

View File

@@ -86,9 +86,12 @@ export default function ForgotPassword() {
<DialogHeader>
<DialogTitle>Command line instructions</DialogTitle>
</DialogHeader>
<p className="text-primary/70 text-[0.95em]">
Use the following command to reset
your password:
<p className="text-primary/70 text-[0.95em] leading-relaxed">
If you've lost the password to your admin account, you may reset it using the following
command.
</p>
<p className="text-primary/70 text-[0.95em] leading-relaxed">
Then log into the backend and reset your user account password in the users table.
</p>
<code className="bg-muted rounded-sm py-0.5 px-2.5 mr-auto text-sm">
beszel admin update youremail@example.com newpassword

View File

@@ -304,7 +304,7 @@ export default function SystemsTable() {
? 'auto'
: cell.column.getSize(),
}}
className={'overflow-hidden relative py-3'}
className={'overflow-hidden relative py-2.5'}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>

View File

@@ -71,21 +71,21 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
system={system}
alerts={systemAlerts}
name="CPU"
title="CPU usage"
title="CPU Usage"
description="Triggers when CPU usage exceeds a threshold."
/>
<AlertWithSlider
system={system}
alerts={systemAlerts}
name="Memory"
title="Memory usage"
title="Memory Usage"
description="Triggers when memory usage exceeds a threshold."
/>
<AlertWithSlider
system={system}
alerts={systemAlerts}
name="Disk"
title="Disk usage"
title="Disk Usage"
description="Triggers when disk usage exceeds a threshold."
/>
</div>
@@ -104,11 +104,11 @@ function AlertStatus({ system, alerts }: { system: SystemRecord; alerts: AlertRe
return (
<label
htmlFor="alert-status"
className="space-y-2 flex flex-row items-center justify-between rounded-lg border p-4 cursor-pointer"
className="flex flex-row items-center justify-between gap-4 rounded-lg border p-4 cursor-pointer"
>
<div className="grid gap-0.5 select-none">
<p className="font-medium text-[1.05em]">System status</p>
<span className="block text-[0.85em] text-foreground opacity-80">
<div className="grid gap-1 select-none">
<p className="font-semibold">System Status</p>
<span className="block text-sm text-foreground opacity-80">
Triggers when status switches between up and down.
</span>
</div>
@@ -171,13 +171,13 @@ function AlertWithSlider({
<div className="rounded-lg border">
<label
htmlFor={`alert-${name}`}
className={cn('space-y-2 flex flex-row items-center justify-between cursor-pointer p-4', {
className={cn('flex flex-row items-center justify-between gap-4 cursor-pointer p-4', {
'pb-0': !!alert,
})}
>
<div className="grid gap-0.5 select-none">
<p className="font-medium text-[1.05em]">{title}</p>
<span className="block text-[0.85em] text-foreground opacity-80">{description}</span>
<div className="grid gap-1 select-none">
<p className="font-semibold">{title}</p>
<span className="block text-sm text-foreground opacity-80">{description}</span>
</div>
<Switch
id={`alert-${name}`}

View File

@@ -20,12 +20,13 @@ export async function copyToClipboard(content: string) {
description: 'Copied to clipboard',
})
} catch (e: any) {
toast({
duration,
description: 'Failed to copy',
})
prompt(
'Automatic copy requires a secure context (https, localhost, or *.localhost). Please copy manually:',
content
)
}
}
const verifyAuth = () => {
pb.collection('users')
.authRefresh()

View File

@@ -1,28 +1,22 @@
# Beszel \*WIP\*
# Beszel
A lightweight server resource monitoring hub with historical data, docker stats, and alerts.
<!-- <table width="100%">
<tbody>
<tr>
<td width="50%"><img src="https://henrygd-assets.b-cdn.net/social-image-server/before-capture.png" alt="example of turso.tech/pricing link which is missing an og:image as of may 11 2024"/></td>
<td width="50%"><img src="https://henrygd-assets.b-cdn.net/social-image-server/after-capture.webp" alt="example of turso.tech/pricing link using an image generated by the server as it's og:image"/></td>
</tr>
</tbody>
</table> -->
[![Docker Image Size (tag)](https://img.shields.io/docker/image-size/henrygd/beszel-agent/0.0.1-alpha.9?logo=docker&label=agent%20image%20size)](https://hub.docker.com/r/henrygd/beszel-agent)
[![Docker Image Size (tag)](https://img.shields.io/docker/image-size/henrygd/beszel/0.0.1-alpha.9?logo=docker&label=hub%20image%20size)](https://hub.docker.com/r/henrygd/beszel)
<!-- ## Features
![Screenshot of the hub](https://henrygd-assets.b-cdn.net/beszel/screenshot.png)
## Features
- **Lightweight**: Much smaller and less demanding than leading solutions.
- **Historical data**: Stats are available for up to 30 days.
- **Docker stats**: CPU and memory usage history for each container.
- **Alerts**: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
- **Simple**: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
- **Alerts**: Configurable alerts for CPU, memory, and disk usage, and system status.
- **Multi-user**: Each user has their own systems. Admins can share systems across users.
- **Secure**: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
- **Oauth / OIDC**: Supports many OAuth2 providers and password auth can be disabled.
- **Automated backups**: Automatically back up your data to S3-compatible storage.
- **Open source**: MIT license and no paywalled features. -->
- **Simple**: Easy setup and doesn't require anything to be publicly available online.
- **OAuth / OIDC**: Supports many OAuth2 providers. Password auth can be disabled.
- **Automatic backups**: Save and restore your data to / from disk or S3-compatible storage.
- **REST API**: Use your metrics in your own scripts and applications.
## Introduction
@@ -32,14 +26,11 @@ The hub is a web application that provides a dashboard to view and manage your c
The agent runs on each system you want to monitor. It creates a minimal SSH server through which it communicates system metrics to the hub.
[![Docker Image Size (tag)](https://img.shields.io/docker/image-size/henrygd/beszel-agent/0.0.1-alpha.3?logo=docker&label=agent%20image%20size)](https://hub.docker.com/r/henrygd/beszel-agent)
[![Docker Image Size (tag)](https://img.shields.io/docker/image-size/henrygd/beszel/0.0.1-alpha.3?logo=docker&label=hub%20image%20size)](https://hub.docker.com/r/henrygd/beszel)
## Getting started
If using the binary instead of docker, ignore 4-5 and run the agent using the binary instead.
1. Start the hub (see [Installation](#installation)). The binary command is `beszel serve`.
1. Start the hub (see [installation](#installation)). The binary command is `beszel serve`.
2. Open http://localhost:8090 and create an admin user.
3. Click "Add system." Enter the name and host of the system you want to monitor.
4. Click "Copy docker compose" to copy the agent's docker-compose.yml file to your clipboard.
@@ -104,8 +95,9 @@ Use `beszel update` and `beszel-agent update` to update to the latest version.
Beszel supports OpenID Connect and many OAuth2 authentication providers (see list below). To enable, do the following:
1. Create an OAuth2 application using your provider of choice. The redirect / callback URL should be `<your-beszel-url>/api/oauth2-redirect`.
2. When you have the client ID and secret, go to the "Auth providers" page and enable your provider.
1. Make sure your "Application URL" is set correctly in the PocketBase settings.
2. Create an OAuth2 application using your provider of choice. The redirect / callback URL should be `<your-beszel-url>/api/oauth2-redirect`.
3. When you have the client ID and secret, go to the "Auth providers" page and enable your provider.
<details>
<summary>Supported provider list</summary>
@@ -144,7 +136,25 @@ The hub and agent communicate over SSH, so they don't need to be exposed to the
When the hub is started for the first time, it generates an ED25519 key pair.
The agent's SSH server is configured to accept connections only using this key. It does not provide a pty or accept any input, so it is not possible to execute commands on the agent even if your private key is compromised.
The agent's SSH server is configured to accept connections only using this key. It does not provide a pseudo-terminal or accept input, so it's not possible to execute commands on the agent even if your private key is compromised.
## User roles
### Admin
Assumed to have an admin account in PocketBase, so links to backups, SMTP settings, etc., are shown in the hub.
The first user created automatically becomes an admin and can log into PocketBase.
Please note that changing a user's role will not create a PocketBase admin account for them. If you want to do that, go to Settings > Admins in PocketBase and add them there.
### User
Can create their own systems and alerts. Links to PocketBase settings are not shown in the hub.
### Read only
Cannot create systems, but can view any system that has been shared with them by an admin. Can create alerts.
## FAQ / Troubleshooting
@@ -164,8 +174,17 @@ If it's not set, the agent will try to find the filesystem mounted on `/` and us
- Run `lsblk` and choose an option under "NAME"
- Run `sudo fdisk -l` and choose an option under "Device"
### Docker containers are not populating reliably
Try upgrading your docker version on the agent system. I had this issue on a machine running version 24. It was fixed by upgrading to version 27.
### Month / week records are not populating reliably
Records for longer time periods are made by averaging stats from the shorter time periods. They require the agent to be running uninterrupted for long enough to get a full set of data.
If you pause / unpause the agent for longer than one minute, the data will be incomplete and the timing for the current interval will reset.
<!--
## Support
My country, the USA, and many others, are actively involved in the genocide of the Palestinian people. I would greatly appreciate any effort you could make to pressure your government to stop enabling this violence. -->