Skip to main content

cctv-stack

CI Publish

Self-hosted CCTV built on Frigate, adding the four things Frigate deliberately leaves to the operator:

  1. Canonical clips — every review becomes one file at a deterministic path.
  2. Durable cloud backup — clips are uploaded to Backblaze B2 exactly once.
  3. Independent retention — local cleanup that never deletes an un-uploaded clip.
  4. Health surfaces — each service reports whether it is actually working.

Every installation is independent: no central hub, no cross-site data, no inbound connectivity requirement.


Status

Capability State
Detect → canonical clip ✅
Upload to Backblaze B2, exactly once ✅
Local retention (upload-aware) ✅
Cloud retention and reconciliation ⬜
Aggregate status page and alerting ⬜
One-command client installer and signed bundle ⬜
Backup, restore, upgrade ⬜

v0.3 is usable for a local-only installation or a B2-backed one. The deferred work, and why it is deferred, is in docs/plans/backlog.md. The client-site delivery plan is docs/plans/packaging.md.

How it fits together

 cameras ──RTSP──> Frigate ──review──> clip-worker ──> data/clips/... ──> uploader ──> B2
                      │                                    │
                      └── recordings (recovery window)      └── cleanup (only after upload)

Frigate does camera ingest, detection, recording and review segmentation. The clip-worker cuts one canonical clip per review item. The uploader pushes it once, and cleanup deletes a local clip only after its upload is terminal-success (INV-1).

MQTT is the low-latency path. The Frigate review sweep is the guarantee: if the broker or the worker was down, the next sweep still materialises the clip. A review arriving twice produces one clip, not two.

Quick start

Requirements: git, Linux x86_64 or arm64 (macOS is supported for development), Docker Engine with the Compose v2 plugin, and disk space for Frigate's recordings. The first real run pulls the Frigate image and builds the CCTV image, so expect several minutes.

git clone https://github.com/aravindmyd/cctv-stack.git
cd cctv-stack
./install.sh

The first run creates the two files that only you can fill in, then exits:

File Contains Committed?
.env Camera RTSP URLs, timezone, optional B2 credentials No — gitignored
config/cameras.yml Camera names, clip padding, retention policy No — gitignored

Edit both, then run ./install.sh again. It validates the configuration, generates Frigate's config, builds the image, starts the stack, and waits until Frigate answers.

Frigate's UI is HTTPS on https://127.0.0.1:8971 (loopback only). The certificate is self-signed, so the browser warns once. Port 5000 and MQTT are never published.

Clips land at:

data/clips/<camera>/YYYY/MM/DD/HH-MM-SS.mp4
data/clips/<camera>/YYYY/MM/DD/HH-MM-SS.mp4.json

The sidecar records the review, event ids, requested boundaries, coverage, size and SHA-256. The clips tree uses the site timezone; Frigate's own recordings tree uses UTC — both are deliberate (ADR-0017).

A clip appears after the visit ends, not during it: Frigate keeps the review open until the alert cutoff passes. There is no partial clip by design.

What runs

Service Role
Frigate Camera ingest, detection, recording and review segmentation
clip-worker Materialises completed reviews at the canonical path, with a sidecar
uploader Uploads to B2 through a storage adapter, with a persisted retry state machine. Reports DISABLED when no bucket is configured and exits 78 on a half-configuration
cleanup Enforces local retention; refuses to delete a clip whose upload is not terminal-success
config One-shot generator: cameras.yml → Frigate's config (INV-3)
mosquitto MQTT broker, internal only
status-collector ⬜ not built yet

Health surfaces

Surface Address Notes
Frigate UI https://127.0.0.1:8971 HTTPS, self-signed, loopback only
Uploader http://127.0.0.1:8082/health DISABLED without a B2 bucket
Cleanup http://127.0.0.1:8083/health Local retention state
Frigate API, MQTT not published Reachable only on the compose network

Configuration

The split is deliberate and load-bearing:

  • config/cameras.yml is the single source of truth for cameras (INV-3). Frigate's config is generated from it and must never be hand-edited.
  • .env holds location values and secrets: RTSP URLs, B2 keys, ports, data path.

Both real files are gitignored. .env.example and config/cameras.example.yml are the templates. Never commit the filled-in versions, and never paste an RTSP URL or B2 key into an issue. If a credential is exposed, follow docs/runbooks/rotate-credentials.md.

Camera names are lowercase kebab-case because they become filesystem paths and cloud object keys.

Published artefacts

Artefact Where Published when
Branch image ghcr.io/aravindmyd/cctv-stack:edge Every merge to main/master
Commit image ghcr.io/aravindmyd/cctv-stack:sha-<short> Every merge
Version image ghcr.io/aravindmyd/cctv-stack:<version> A v* tag
Python package pypi.org/project/cctv-stack A v* tag
Wheel and sdist Workflow artefact, and GitHub Release on tags Every merge / tag
docker pull ghcr.io/aravindmyd/cctv-stack:edge
pip install cctv-stack

The image is the runtime. pip install cctv-stack installs the four CLI entry points (cctv-clip-worker, cctv-config, cctv-uploader, cctv-cleanup) but not Frigate, Compose, or site configuration. The complete client-site install is the signed release bundle in docs/plans/packaging.md.

Development

uv sync
uv run pytest                    # unit suite; live tests skip without Docker/S3
uv run ruff check .
uv run ruff format --check .

./tests/integration/run.sh       # live S3 suite against RustFS; needs Docker
./tests/e2e/run.sh               # synthetic camera → real clip; needs Docker + Frigate

The unit suite is 717 passed, 19 skipped without a configured S3 endpoint or a running Docker daemon. CI runs pre-commit, unit tests on Python 3.12 and 3.13, the pip-install fallback, live integration, and the synthetic e2e pipeline. The publish workflow builds the multi-arch image and the Python distributions.

Documentation

License

Apache-2.0 — see LICENSE. The reasoning is in docs/plans/opensourcing.md.

Metadata

Release files for cctv-stack 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cctv-stack 0.3.0
File Size Uploaded Provenance
cctv_stack-0.3.0.tar.gz 119.1 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for cctv-stack 0.3.0
File Interpreter ABI Platform
cctv_stack-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 255.8 kB

Release files / cctv_stack-0.3.0.tar.gz

Download URL cctv_stack-0.3.0.tar.gz
Size 119.1 kB
Tags Source
SHA-256 checksum
How to use checksums
d9d76f7621b2e0cde274d243fa3b562a30342a02e49b0209ead239e036132b26
BLAKE2b-256 checksum
How to use checksums
a8af021671fcfd18cbb266412820a377ff741da24882b1ede180e3b61e032806
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / cctv_stack-0.3.0-py3-none-any.whl

Download URL cctv_stack-0.3.0-py3-none-any.whl
Size 136.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a8ee061da165eb38423aebc6fafea26e27e6ac2b5e310043e5cf070036e92cef
BLAKE2b-256 checksum
How to use checksums
e0890fbca469a627182fa82185cc9b3c90a7a4e24545e73460319328533b83b9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Security for cctv-stack 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for cctv_stack-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page