Applications & App Store
The App Store lets you deploy containerized workloads to any Kubernetes-enabled device in your fleet. Apps are defined as Kubernetes manifests paired with a metadata.yaml file, and can be sourced from the built-in library, your own Git repositories, or Helm charts.
How It Works
App Source (Git / Helm / Local)
│
▼
Repository Manager ←── sync on schedule or manual trigger
│
▼
App Catalog ←── browse, search, filter by tag
│
▼
Configure Fields ←── fill in API keys, settings, secrets
│
▼
Template Substitution ←── ${MY_FIELD} replaced in manifests
│
▼
kubectl apply ──→ target device's K3s cluster
App Sources
| Type | Description |
|---|---|
| Local | Built-in apps shipped with Watchgrid (in apps/ directory) |
| Git | Any public or private Git repository |
| Helm | Helm chart repositories |
Adding an App Repository
Go to Applications → Repository Manager (gear icon in the top right).
Git Repository
Create a Git repository that follows the app directory layout, then add it to Watchgrid:
| Field | Example |
|---|---|
| Name | my-apps |
| Type | git |
| URL | https://github.com/youruser/watchgrid-apps.git |
| Branch | main |
| Username | (optional, for private repos) |
| Password | (optional, personal access token) |
| SSH Private Key | (optional, PEM-encoded key contents) |
For SSH-based access (git@host:org/repo.git), paste the contents of the PEM private key into the SSH Private Key field — Watchgrid stores it in the database and writes it to a temp file (mode 0600) only for the duration of each sync. The server uses it via GIT_SSH_COMMAND=ssh -i <tmp> -o IdentitiesOnly=yes -o StrictHostKeyChecking=no. Stored secrets (password and SSH key) are never returned by the API — repository listings expose has_password / has_ssh_key booleans instead.
The repository URL must use one of the supported transports — https://, http://, ssh://, git://, or scp-style git@host:org/repo.git. Other transports (including git's ext:: and file://) are rejected. The name, branch and path must not contain path-traversal sequences (..) or begin with -; the name is also used as the on-disk clone directory, so it is restricted to a simple identifier.
Once added, Watchgrid clones the repo and scans it for apps. Repositories sync automatically on a configurable interval (default 5 minutes); a background scheduler pulls each enabled repository whose interval has elapsed. Every sync operation (git and helm) is bounded by a 5-minute timeout, so a hung remote can't stall the scheduler. Use Sync for an immediate force-sync at any time, or Sync All to fire every repository in parallel — each surfaces its status (Syncing…, Synced at HH:MM:SS, or Sync failed: <reason>) inline. Each repository row shows its auto-sync interval, last sync time (persisted across restarts), and the last sync status/error.
Use Edit on a repository to change its URL, branch, path, credentials (leave password/SSH key blank to keep the stored secret), the sync interval, or to disable automatic sync. Editing is also where you pause a repository from auto-syncing without removing it.
Helm Repository
| Field | Example |
|---|---|
| Name | bitnami |
| Type | helm |
| URL | https://charts.bitnami.com/bitnami |
| Chart | nginx |
App Directory Layout
Each app lives in its own subdirectory within the repository:
my-apps-repo/
nginx-proxy/
metadata.yaml ← app definition and config fields
deployment.yaml ← Kubernetes Deployment + Service
mqtt-broker/
metadata.yaml
deployment.yaml
configmap.yaml ← any number of manifest files
grafana/
metadata.yaml
deployment.yaml
pvc.yaml
secret.yaml
All .yaml / .yml files except metadata.yaml are treated as deployable manifests and applied together on deploy.
Writing metadata.yaml
metadata.yaml defines how your app appears in the catalog and what configuration fields users fill in before deploying.
Minimal Example
name: "Nginx Proxy"
description: "A simple reverse proxy"
version: "1.0.0"
namespace: "default"
author: "Your Name"
tags:
- "web"
- "proxy"
Full Example with Config Fields
name: "MQTT Broker"
description: "Mosquitto MQTT broker for IoT devices"
version: "2.0.1"
namespace: "iot"
author: "Your Name"
tags:
- "iot"
- "mqtt"
- "networking"
icon: "📡"
expose_port: 1883
expose_protocol: tcp
min_replicas: 1
max_replicas: 3
default_replicas: 1
config_fields:
- name: "ADMIN_PASSWORD"
type: "secret"
description: "Broker admin password"
required: true
- name: "MAX_CONNECTIONS"
type: "string"
description: "Maximum concurrent client connections"
default: "1000"
required: false
- name: "ALLOW_ANONYMOUS"
type: "boolean"
description: "Allow anonymous client connections"
default: "false"
required: false
Metadata Fields
| Field | Type | Description |
|---|---|---|
name |
string | Display name in the app catalog |
description |
string | Short description shown in the catalog card |
version |
string | App version label |
namespace |
string | Default K8s namespace (default if omitted) |
author |
string | Informational — shown in app details |
tags |
list | Used for search and filtering |
icon |
string | Emoji shown in the catalog card |
expose_port |
int | Primary port the app listens on |
expose_protocol |
string | http, https, tcp, udp |
min_replicas |
int | Minimum replica count |
max_replicas |
int | Maximum replica count |
default_replicas |
int | Starting replica count |
config_fields |
list | Fields shown in the configure modal |
config_fields Schema
| Field | Type | Description |
|---|---|---|
name |
string | Key used in manifest template substitution |
type |
string | string, secret, or boolean |
description |
string | Help text shown in the UI |
default |
string | Pre-filled default value |
required |
bool | Blocks deployment if empty |
UI rendering:
- string → text input
- secret → masked password input
- boolean → true/false dropdown
Writing the Manifest
Kubernetes manifests are standard YAML. Use ${FIELD_NAME} placeholders anywhere you want user-provided values substituted.
Example: deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mqtt-broker
labels:
app: mqtt-broker
spec:
replicas: 1
selector:
matchLabels:
app: mqtt-broker
template:
metadata:
labels:
app: mqtt-broker
spec:
containers:
- name: mosquitto
image: eclipse-mosquitto:2
ports:
- containerPort: 1883
env:
- name: ALLOW_ANONYMOUS
value: "${ALLOW_ANONYMOUS}"
- name: MAX_CONNECTIONS
value: "${MAX_CONNECTIONS}"
resources:
limits:
memory: "128Mi"
cpu: "500m"
requests:
memory: "64Mi"
cpu: "100m"
---
apiVersion: v1
kind: Secret
metadata:
name: mqtt-broker-secret
type: Opaque
stringData:
ADMIN_PASSWORD: "${ADMIN_PASSWORD}"
---
apiVersion: v1
kind: Service
metadata:
name: mqtt-broker-service
spec:
selector:
app: mqtt-broker
ports:
- name: mqtt
protocol: TCP
port: 1883
targetPort: 1883
type: ClusterIP
How Template Substitution Works
Before applying the manifest, Watchgrid replaces all ${FIELD_NAME} placeholders with the values the user entered in the configure modal.
Substitution happens in three ways:
- Direct string replacement —
${FIELD_NAME}anywhere in the manifest - Environment variables —
- name: FIELD_NAMEfollowed byvalue:is updated automatically - Secret
stringData— keys instringDatamatching the field name are updated automatically
Configuration values are persisted per device — redeploying the same app on the same device reuses the saved configuration.
Deploying an App
- Select a device — only Kubernetes-enabled devices appear
- Select a namespace — discovered from the target device
- Browse the catalog — search by name or filter by tag
- Click Deploy on an app
- Fill in config fields — required fields must be completed
- Deploy or Install:
- Deploy — applies the manifest and starts the app immediately
- Install — downloads the app without starting it
Managing Deployed Apps
The deployed apps list shows per-device status with the following actions:
| Action | Description |
|---|---|
| Start | Start a stopped app |
| Stop | Scale replicas to zero (preserves config) |
| Uninstall | Remove all Kubernetes resources for this app |
Updates, Upgrades & Rollback
Watchgrid tracks which image digest every deploy shipped, so pushing a new
container to the internal registry — even under a reused tag
like :latest — is detected automatically.
How detection works
- At deploy time the server records the app's internal-registry images and their digests per device (the deployment ledger).
- A background watcher re-resolves the digest of every tracked
repo:tag(default every 60 seconds, tunable viaWATCHGRID_REGISTRY_POLL_SECONDS). For instant detection you can additionally configure the registry's native notifications webhook — see Docker Registry → Push notifications. - When the digest for a tag changes, affected deployments show Update available in App Store → Deployments and on the app's store card.
Deployments tab (App Store → Deployments) lists every tracked deployment: app, device, image, the digest it runs, the newest pushed digest, and actions:
| Action | Description |
|---|---|
| Upgrade | Redeploys with images pinned to the newest digest (image:tag@sha256:...), forcing a real rollout — also for :latest tags |
| Roll back | Redeploys pinned to the digest that ran before the last upgrade |
Auto-update per site — in a site's Applications tab, each app has an Auto-update toggle next to Auto-deploy. When enabled, a new push to the registry automatically rolls out a digest-pinned redeploy to that site's devices. Every automatic rollout is recorded in the audit log.
Deployments are recorded from the first deploy after upgrading to 1.32 — redeploy an existing app once to start tracking it.
Routines (Scheduling)
Routines let you schedule recurring actions on deployed apps.
Go to Applications → Routines and create a routine:
| Field | Description |
|---|---|
| Action | start, stop, or restart |
| Schedule | Cron expression — e.g. 0 2 * * * for 2:00 AM daily |
| Target | Specific app on a specific device |
| Timezone | Configurable per routine |
Example: Restart your monitoring stack every night at 3:00 AM: