feat(install): add bootstrap installer and simplify README

This commit is contained in:
2026-06-20 21:42:34 -05:00
parent 8a2eb31baa
commit f07be8a531
2 changed files with 450 additions and 141 deletions

175
README.md
View File

@@ -1,152 +1,45 @@
![Maintainarr Banner](web/static/img/maintainarr_banner.png)
# Maintainarr # Maintainarr
Maintainarr is a self-hosted Go application for managing a single organization's Linux VM fleet from one dashboard. Maintainarr is a self-hosted dashboard for managing Linux VMs from one place.
It ships with: ## Install
- SQLite-backed persistence ```bash
- Daily compressed command log archives curl -fsSL https://dock-it.dev/Idea-Studios/Maintainarr/raw/branch/main/install.sh | sudo bash
- User registration and login
- OTP-based 2FA setup and verification
- Role model with `admin`, `editor`, and `viewer`
- Sidebar dashboard with theme tokens compatible with [UI Colors](https://uicolors.app/generate)
- VM chip grid with distro icon, CPU/RAM bars, IP, and uptime
- Node overview pages with action buttons and SSH web console
- Wake-on-LAN, restart, shutdown, stat refresh, and apt-upgrade action hooks
- VM groups and recurring/triggered automation job scaffolding
- Go-first backend structure intended for adding more fleet features without rewrites
## Branch Model
- `main`: stable branch
- `dev`: active feature branch
The repository has been initialized with both `main` and `dev`. Work should land in `dev` first.
## Stack
- Go `1.25`
- `chi` router
- `modernc.org/sqlite` for embedded SQLite
- `gorilla/sessions` for cookie sessions
- `pquerna/otp` for TOTP 2FA
- `golang.org/x/crypto/ssh` for remote command and console transport
- Embedded Go HTML templates plus local static assets
## Project Layout
```text
cmd/maintainarr entrypoint
internal/app app bootstrap and routing
internal/config environment configuration
internal/db sqlite connection and schema bootstrap
internal/handlers HTTP handlers and websocket console
internal/middleware auth and role enforcement
internal/models domain models
internal/services auth, crypto, nodes, scheduler, repository
internal/views embedded templates and view helpers
web/static CSS and browser-side JS
``` ```
## Run The installer will:
```powershell - install missing system packages
- install Go if needed
- download or reuse Maintainarr under `/opt/maintainarr`
- create config in `/etc/maintainarr/maintainarr.env`
- create data in `/var/lib/maintainarr`
- ask about auto-updates with `cron`
- ask about starting at boot with `systemd`
## Open
After install, open:
```text
http://your-server-ip:8080
```
The first registered user becomes the admin.
## Manual Run
```bash
cd /opt/maintainarr/src
go run ./cmd/maintainarr go run ./cmd/maintainarr
``` ```
Default address: `http://localhost:8080` ## Important Paths
## Container - config: `/etc/maintainarr/maintainarr.env`
- data: `/var/lib/maintainarr`
```powershell - source: `/opt/maintainarr/src`
docker build -t maintainarr . - binary: `/opt/maintainarr/bin/maintainarr`
docker run --rm -p 8080:8080 -v ${PWD}/data:/app/data maintainarr
```
## First User
The first registered user becomes the initial `admin`.
No example users, groups, nodes, or jobs are seeded.
## Environment
```env
MAINTAINARR_ADDR=:8080
MAINTAINARR_DB_PATH=data/maintainarr.db
MAINTAINARR_LOG_ARCHIVE_DIR=data/log-archives
MAINTAINARR_SESSION_KEY=change-me-session-key-please
MAINTAINARR_ENCRYPTION_KEY=change-me-encryption-key-32bytes
MAINTAINARR_ORG_NAME=Maintainarr
MAINTAINARR_BASE_URL=http://localhost:8080
MAINTAINARR_THEME=blue
MAINTAINARR_THEME_MODE=dark
MAINTAINARR_REFRESH_CRON=@every 5s
```
## Release Automation
- Push or merge into `main` to trigger `.gitea/workflows/release.yml`
- The workflow builds a Docker image, publishes it to the Gitea container registry, and creates or updates a Gitea release
- It tags the image as `latest`, `main`, and the short commit SHA
- Required secret: `GITEA_TOKEN`
- Optional secret: `GITEA_REGISTRY`
Defaults to the host from `gitea.server_url`
- Optional secret: `GITEA_REGISTRY_USERNAME`
Required for container registry login
- Optional secret: `GITEA_REGISTRY_TOKEN`
Defaults to `GITEA_TOKEN`
- Optional secret: `GITEA_PACKAGE_NAMESPACE`
Defaults to the repository owner from `gitea.repository`
## Roles
- `admin`: full access, intended for user management and future organization settings
- `editor`: can operate nodes and create automation jobs
- `viewer`: read-only access
## Theme System
The dashboard is styled around `uicolors.app` style primary scale variables:
```css
--color-primary-50
--color-primary-100
--color-primary-200
--color-primary-300
--color-primary-400
--color-primary-500
--color-primary-600
--color-primary-700
--color-primary-800
--color-primary-900
--color-primary-950
```
Replace those values in `web/static/css/app.css` or feed them through a future admin theme editor to recolor the interface.
## Current Scope
This repository now includes the base architecture for:
- auth and sessions
- role-aware route protection
- dashboard rendering
- node actions
- SSH-backed console transport
- SQLite schema for future fleet features
- recurring automation scheduling
## Next Expansions
The backend shape is ready for:
- user management screens
- node create/edit flows
- SSH key auth
- richer live metrics collection
- audit logs
- package policy models
- triggered event ingestion
- per-group maintenance windows
- multi-step provisioning workflows

416
install.sh Executable file
View File

@@ -0,0 +1,416 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_URL="https://dock-it.dev/Idea-Studios/Maintainarr.git"
REPO_BRANCH="${MAINTAINARR_BRANCH:-main}"
APP_USER="${MAINTAINARR_USER:-maintainarr}"
INSTALL_ROOT="${MAINTAINARR_INSTALL_ROOT:-/opt/maintainarr}"
SRC_DIR="$INSTALL_ROOT/src"
BIN_DIR="$INSTALL_ROOT/bin"
BIN_PATH="$BIN_DIR/maintainarr"
DATA_DIR="${MAINTAINARR_DATA_DIR:-/var/lib/maintainarr}"
CONFIG_DIR="${MAINTAINARR_CONFIG_DIR:-/etc/maintainarr}"
ENV_FILE="$CONFIG_DIR/maintainarr.env"
SYSTEMD_UNIT="/etc/systemd/system/maintainarr.service"
UPDATE_SCRIPT="/usr/local/bin/maintainarr-update"
CRON_FILE="/etc/cron.d/maintainarr-update"
GO_VERSION="${MAINTAINARR_GO_VERSION:-1.26.4}"
log() {
printf '[maintainarr] %s\n' "$*"
}
fail() {
printf '[maintainarr] ERROR: %s\n' "$*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fail "Run this installer as root, for example: curl -fsSL ... | sudo bash"
fi
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
apt_package_installed() {
dpkg -s "$1" >/dev/null 2>&1
}
version_ge() {
[[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" == "$2" ]]
}
detect_arch() {
case "$(uname -m)" in
x86_64|amd64)
echo "amd64"
;;
aarch64|arm64)
echo "arm64"
;;
*)
fail "Unsupported architecture: $(uname -m)"
;;
esac
}
pkg_install() {
if command_exists apt-get; then
export DEBIAN_FRONTEND=noninteractive
apt-get install -y "$@"
return
fi
if command_exists dnf; then
dnf install -y "$@"
return
fi
if command_exists yum; then
yum install -y "$@"
return
fi
if command_exists pacman; then
pacman -Sy --noconfirm "$@"
return
fi
if command_exists zypper; then
zypper --non-interactive install "$@"
return
fi
if command_exists apk; then
apk add --no-cache "$@"
return
fi
fail "No supported package manager found"
}
install_base_packages() {
if command_exists apt-get; then
local packages=()
local package
for package in ca-certificates curl git tar cron; do
if ! apt_package_installed "$package"; then
packages+=("$package")
fi
done
if [[ "${#packages[@]}" -eq 0 ]]; then
log "System packages already present: ca-certificates curl git tar cron"
return
fi
log "Installing missing system packages: ${packages[*]}"
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y "${packages[@]}"
return
fi
if command_exists dnf; then
pkg_install ca-certificates curl git tar cronie
return
fi
if command_exists yum; then
pkg_install ca-certificates curl git tar cronie
return
fi
if command_exists pacman; then
pkg_install ca-certificates curl git tar cronie
return
fi
if command_exists zypper; then
pkg_install ca-certificates curl git tar cron
return
fi
if command_exists apk; then
pkg_install ca-certificates curl git tar dcron
return
fi
fail "Unable to install prerequisites for this distribution"
}
install_go() {
local current_version=""
if command_exists go; then
current_version="$(go version | awk '{print $3}' | sed 's/^go//')"
elif [[ -x /usr/local/go/bin/go ]]; then
current_version="$(/usr/local/go/bin/go version | awk '{print $3}' | sed 's/^go//')"
fi
if [[ -n "$current_version" ]] && version_ge "$current_version" "$GO_VERSION"; then
log "Go $current_version already satisfies the requirement"
return
fi
local arch
arch="$(detect_arch)"
local archive="/tmp/go${GO_VERSION}.linux-${arch}.tar.gz"
log "Installing Go $GO_VERSION"
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${arch}.tar.gz" -o "$archive"
rm -rf /usr/local/go
tar -C /usr/local -xzf "$archive"
ln -sf /usr/local/go/bin/go /usr/local/bin/go
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
}
tty_available() {
[[ -r /dev/tty && -w /dev/tty ]]
}
prompt_yes_no() {
local prompt="$1"
local default_answer="$2"
local reply=""
local suffix="[y/N]"
if [[ "$default_answer" == "y" ]]; then
suffix="[Y/n]"
fi
if ! tty_available; then
[[ "$default_answer" == "y" ]]
return
fi
while true; do
printf '%s %s ' "$prompt" "$suffix" > /dev/tty
IFS= read -r reply < /dev/tty || true
reply="${reply:-$default_answer}"
case "$reply" in
y|Y|yes|YES)
return 0
;;
n|N|no|NO)
return 1
;;
*)
printf 'Please answer yes or no.\n' > /dev/tty
;;
esac
done
}
ensure_user() {
if id -u "$APP_USER" >/dev/null 2>&1; then
return
fi
local nologin_shell
nologin_shell="$(command -v nologin || true)"
nologin_shell="${nologin_shell:-/usr/sbin/nologin}"
useradd --system --home "$INSTALL_ROOT" --create-home --shell "$nologin_shell" "$APP_USER"
}
clone_or_update_repo() {
mkdir -p "$INSTALL_ROOT" "$BIN_DIR"
if [[ -d "$SRC_DIR/.git" ]]; then
log "Updating source in $SRC_DIR"
git -C "$SRC_DIR" fetch --tags origin
git -C "$SRC_DIR" checkout "$REPO_BRANCH"
git -C "$SRC_DIR" pull --ff-only origin "$REPO_BRANCH"
return
fi
if [[ -f "$SRC_DIR/go.mod" && -d "$SRC_DIR/cmd/maintainarr" ]]; then
log "Existing Maintainarr source found in $SRC_DIR; skipping download"
return
fi
log "Cloning source into $SRC_DIR"
git clone --branch "$REPO_BRANCH" --single-branch "$REPO_URL" "$SRC_DIR"
}
build_binary() {
log "Building Maintainarr"
mkdir -p "$BIN_DIR"
(
cd "$SRC_DIR"
export PATH="/usr/local/go/bin:/usr/local/bin:$PATH"
export CGO_ENABLED=0
go build -o "$BIN_PATH" ./cmd/maintainarr
)
chmod 755 "$BIN_PATH"
}
random_hex() {
local bytes="$1"
od -An -N"$bytes" -tx1 /dev/urandom | tr -d ' \n'
}
write_env_file() {
mkdir -p "$CONFIG_DIR" "$DATA_DIR/log-archives"
if [[ -f "$ENV_FILE" ]]; then
log "Keeping existing environment file at $ENV_FILE"
return
fi
local session_key
local encryption_key
session_key="$(random_hex 32)"
encryption_key="$(random_hex 16)"
cat > "$ENV_FILE" <<EOF
MAINTAINARR_ADDR=:8080
MAINTAINARR_DB_PATH=$DATA_DIR/maintainarr.db
MAINTAINARR_LOG_ARCHIVE_DIR=$DATA_DIR/log-archives
MAINTAINARR_SESSION_KEY=$session_key
MAINTAINARR_ENCRYPTION_KEY=$encryption_key
MAINTAINARR_ORG_NAME=Maintainarr
MAINTAINARR_BASE_URL=http://localhost:8080
MAINTAINARR_THEME=blue
MAINTAINARR_THEME_MODE=dark
MAINTAINARR_REFRESH_CRON=@every 5s
EOF
chmod 640 "$ENV_FILE"
}
write_update_script() {
cat > "$UPDATE_SCRIPT" <<EOF
#!/usr/bin/env bash
set -euo pipefail
export PATH="/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin"
APP_USER="$APP_USER"
SRC_DIR="$SRC_DIR"
BIN_PATH="$BIN_PATH"
REPO_BRANCH="$REPO_BRANCH"
if [[ ! -d "\$SRC_DIR/.git" ]]; then
echo "Maintainarr source directory not found: \$SRC_DIR" >&2
exit 1
fi
git -C "\$SRC_DIR" fetch --tags origin
git -C "\$SRC_DIR" checkout "\$REPO_BRANCH"
git -C "\$SRC_DIR" pull --ff-only origin "\$REPO_BRANCH"
(
cd "\$SRC_DIR"
export CGO_ENABLED=0
go build -o "\$BIN_PATH" ./cmd/maintainarr
)
chown "\$APP_USER:\$APP_USER" "\$BIN_PATH"
if command -v systemctl >/dev/null 2>&1 && systemctl is-enabled --quiet maintainarr 2>/dev/null; then
systemctl restart maintainarr
fi
EOF
chmod 755 "$UPDATE_SCRIPT"
}
write_systemd_service() {
cat > "$SYSTEMD_UNIT" <<EOF
[Unit]
Description=Maintainarr service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=$APP_USER
Group=$APP_USER
WorkingDirectory=$SRC_DIR
EnvironmentFile=$ENV_FILE
ExecStart=$BIN_PATH
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
}
enable_start_on_boot() {
if ! command_exists systemctl; then
log "systemd is not available; skipping boot-start setup"
return
fi
write_systemd_service
systemctl daemon-reload
systemctl enable --now maintainarr
log "Enabled and started maintainarr.service"
}
configure_auto_updates() {
write_update_script
cat > "$CRON_FILE" <<EOF
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
17 3 * * * root $UPDATE_SCRIPT >> /var/log/maintainarr-update.log 2>&1
EOF
chmod 644 "$CRON_FILE"
if command_exists systemctl; then
if systemctl list-unit-files | grep -q '^cron\.service'; then
systemctl enable --now cron >/dev/null 2>&1 || true
elif systemctl list-unit-files | grep -q '^crond\.service'; then
systemctl enable --now crond >/dev/null 2>&1 || true
fi
fi
log "Installed daily update job in $CRON_FILE"
}
main() {
require_root
install_base_packages
install_go
ensure_user
clone_or_update_repo
write_env_file
build_binary
write_update_script
mkdir -p "$DATA_DIR"
chown -R "$APP_USER:$APP_USER" "$INSTALL_ROOT" "$DATA_DIR"
chown root:"$APP_USER" "$ENV_FILE"
if prompt_yes_no "Enable automatic daily updates with cron?" "n"; then
configure_auto_updates
else
rm -f "$CRON_FILE"
log "Skipped cron auto-update setup"
fi
if prompt_yes_no "Start Maintainarr at boot with systemd?" "y"; then
enable_start_on_boot
else
log "Skipped boot-start setup"
log "Run it manually with: $BIN_PATH"
fi
log "Install complete"
log "Config: $ENV_FILE"
log "Data: $DATA_DIR"
log "Source: $SRC_DIR"
log "Binary: $BIN_PATH"
}
main "$@"