Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,5 @@ CODEX.md
.cursor/
.aider*
.copilot/
graphify-out/
*.pt
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,17 @@ Server settings in `configs/server.yaml` or via `CLOCKD_` environment variables:
| `max_upload_mb` | `200` | Max upload size |
| `max_workers` | `2` | Async job thread pool size |
| `cameras_dir` | `configs/cameras` | Camera configs directory |
| `max_cameras` | `50` | Max camera configs the API will create via `POST /cameras` |

Environment variables take precedence over `server.yaml`. Nested settings use `__` as the delimiter, which lets you keep secrets (NVR passwords, InfluxDB tokens) out of the config file entirely and inject them at deploy time — e.g. from a Kubernetes Secret:

```
CLOCKD_METRICS__INFLUXDB_V2__TOKEN=...
CLOCKD_EVENT_SOURCES__HOME_NVR__UNIFI__USERNAME=clockd-user
CLOCKD_EVENT_SOURCES__HOME_NVR__UNIFI__PASSWORD=...
```

A complete hardened Kubernetes deployment (secrets via env vars, non-root, read-only root filesystem, dropped capabilities) is provided at [`deploy/k8s-example.yaml`](deploy/k8s-example.yaml).

### Detection Backends

Expand Down
7 changes: 7 additions & 0 deletions configs/server.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Any setting here can also be set via a CLOCKD_* environment variable, which
# takes precedence over this file. Nested fields use "__" as the delimiter:
# CLOCKD_METRICS__INFLUXDB_V2__TOKEN
# CLOCKD_EVENT_SOURCES__HOME_NVR__UNIFI__PASSWORD
# Use env vars to keep secrets out of this file (see deploy/k8s-example.yaml).

host: "0.0.0.0"
port: 8000
verbose: false
Expand All @@ -9,6 +15,7 @@ max_upload_mb: 200
max_workers: 2
job_ttl_seconds: 3600
cameras_dir: "configs/cameras"
max_cameras: 50
upload_dir: "/tmp/clockd_uploads"

codeproject_ai:
Expand Down
176 changes: 176 additions & 0 deletions deploy/k8s-example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Example Kubernetes deployment for Clockd.
#
# Highlights:
# - Secrets (NVR password, InfluxDB token) are injected as CLOCKD_* env vars
# from a Secret and never stored in the ConfigMap. Env vars override
# server.yaml; nested fields use "__" as the delimiter.
# - Hardened pod security context: non-root, read-only root filesystem,
# all capabilities dropped, default seccomp profile, no privilege
# escalation. Uploads and Ultralytics settings write to an emptyDir
# mounted at /tmp.
#
# Adjust the namespace, storage class, image, and camera map for your cluster.
apiVersion: v1
kind: Namespace
metadata:
name: clockd
---
apiVersion: v1
kind: ConfigMap
metadata:
name: clockd-config
namespace: clockd
data:
server.yaml: |
host: "0.0.0.0"
port: 8000
detection_backend: "local"
model: "yolo26n.pt"
confidence: 0.3
default_unit: "mph"
max_upload_mb: 200
max_workers: 2
max_cameras: 50
cameras_dir: "/app/configs/cameras"
upload_dir: "/tmp/clockd_uploads"

metrics:
influxdb_v2:
enabled: false
url: "http://influxdb.monitoring:8086"
org: "home"
bucket: "clockd"
# token is injected via CLOCKD_METRICS__INFLUXDB_V2__TOKEN

event_sources:
home_nvr:
enabled: false
camera_map:
"your-protect-camera-id": "your-clockd-camera-id"
unit: "mph"
unifi:
host: "192.168.1.1"
verify_ssl: false
poll_interval_s: 30
event_end_timeout_s: 300
# username/password are injected via
# CLOCKD_EVENT_SOURCES__HOME_NVR__UNIFI__USERNAME / __PASSWORD
---
apiVersion: v1
kind: Secret
metadata:
name: clockd-secrets
namespace: clockd
type: Opaque
stringData:
CLOCKD_EVENT_SOURCES__HOME_NVR__UNIFI__USERNAME: "clockd-user"
CLOCKD_EVENT_SOURCES__HOME_NVR__UNIFI__PASSWORD: "change-me"
CLOCKD_METRICS__INFLUXDB_V2__TOKEN: "change-me"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: clockd-cameras
namespace: clockd
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: clockd
namespace: clockd
labels:
app: clockd
spec:
replicas: 1
selector:
matchLabels:
app: clockd
template:
metadata:
labels:
app: clockd
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
# If your storage provider needs it for the cameras PVC, set fsGroup
# to the clockd user's group id from the image.
containers:
- name: clockd
image: ghcr.io/your-registry/clockd:latest
ports:
- containerPort: 8000
name: http
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
env:
# Ultralytics writes its settings file on import; point it at the
# writable tmpfs since the root filesystem is read-only.
- name: YOLO_CONFIG_DIR
value: /tmp/ultralytics
envFrom:
- secretRef:
name: clockd-secrets
volumeMounts:
- name: config
mountPath: /app/configs/server.yaml
subPath: server.yaml
readOnly: true
- name: cameras
mountPath: /app/configs/cameras
- name: tmp
mountPath: /tmp
resources:
requests:
cpu: "1"
memory: 1Gi
limits:
cpu: "4"
memory: 4Gi
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: config
configMap:
name: clockd-config
- name: cameras
persistentVolumeClaim:
claimName: clockd-cameras
- name: tmp
emptyDir:
sizeLimit: 4Gi
---
apiVersion: v1
kind: Service
metadata:
name: clockd
namespace: clockd
labels:
app: clockd
spec:
selector:
app: clockd
ports:
- name: http
port: 8000
targetPort: http
Loading
Loading