Skip to main content

cctv-stack

PyPI Python versions CI

cctv-stack is a Python toolkit and container image for a self-hosted CCTV pipeline built on Frigate.

Frigate does camera ingest, detection, recording and review segmentation. cctv-stack adds the four pieces it leaves to the operator:

  1. Canonical clips — one deterministic file per review item.
  2. Durable backup — clips are uploaded to Backblaze B2 exactly once.
  3. Upload-aware retention — local cleanup never deletes a clip that is not safely uploaded.
  4. Health surfaces — the uploader and cleanup expose /health and /metrics.

It is not a replacement for Frigate. It is the missing layer around it.

Current release: 0.7.0. Clips, B2 upload, upload-aware local retention, the aggregate status page with Gatus/ntfy alerting, a guided one-command installer, ./scripts/doctor.sh diagnosis, encrypted backups, ./update.sh upgrades, and two days of continuous footage kept locally. Cloud retention is not — see CONTRIBUTING.md for the exact state and roadmap.


Install

The shortest path is one command, from a checkout:

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

It asks for the location, timezone, cameras and optional B2 credentials, validates the result through the same loader the services use, detects the host, starts the stack, and waits for Frigate. See docs/install.md for the released-image path and the by-hand alternative.

The Python package is usable on its own for the parts that read Frigate's API:

pip install cctv-stack

Requires Python 3.12+. The distribution is cctv-stack; the import package is cctv.

Optional extras:

Extra Adds Needed by
mqtt paho-mqtt cctv-clip-worker when subscribing to frigate/reviews
storage boto3 cctv-uploader for B2/S3
dev pytest, ruff Contributors
pip install "cctv-stack[mqtt,storage]"

Container image

docker pull ghcr.io/aravindmyd/cctv-stack:0.7.0

Tags published per release: :0.7.0, :0.7, and the commit tag :sha-<short>. A moving :edge tag follows master. If the registry package is private, run docker login ghcr.io first. The image includes ffmpeg/ffprobe.

Commands

Command What it does
cctv-config validate --cameras cameras.yml Validate the camera configuration. Exits 78 (EX_CONFIG) on an invalid config
cctv-config generate --cameras cameras.yml --out frigate.yml Generate Frigate's configuration from cameras.yml
cctv-clip-worker Turn completed Frigate reviews into canonical clips
cctv-uploader Upload clips to B2 through the persisted retry state machine
cctv-cleanup Enforce local retention after a successful upload
cctv-status Serve the aggregate status page (:8084/status), health and metrics
cctv-host Detect this machine's host profile and Frigate's shm_size
cctv-init Answer the install questions and write cameras.yml and .env
cctv-doctor Diagnose a whole installation, read-only (./scripts/doctor.sh)
cctv-backup Encrypted config bundle, online database backup, verification
cctv-migrate Apply pending migrations between stopping and starting services

The four services are long-running and configuration-driven; they take no command-line arguments. Run them under systemd, Docker, or Compose.

With Compose, the answer to is my CCTV working? is at http://<host>:8084/status; the dashboard and history are at http://<host>:3001 (Gatus), and alerts arrive through ntfy at http://<host>:8085.

Quick start

The only required input is a cameras.yml. This minimal file is valid; ${VAR} values are read from the environment and an unset variable is a hard error.

site:
  location: home
  timezone: UTC

cameras:
  front-door:
    display_name: Front Door
    rtsp:
      main: ${CAMERA_FRONT_DOOR_RTSP}
      sub: ${CAMERA_FRONT_DOOR_SUB}

Validate it and generate Frigate's config:

export CAMERA_FRONT_DOOR_RTSP='rtsp://user:pass@192.168.1.10:554/stream1'
export CAMERA_FRONT_DOOR_SUB='rtsp://user:pass@192.168.1.10:554/stream2'

cctv-config validate --cameras cameras.yml
cctv-config generate --cameras cameras.yml --out frigate.yml

validate checks the camera model and the environment substitution. generate writes a Frigate config with a GENERATED FILE — DO NOT EDIT header.

Running the services

export CCTV_CONFIG_DIR="$PWD"
export CCTV_DATA_DIR="$PWD/data"
export CCTV_FRIGATE_URL="http://127.0.0.1:5000"

cctv-clip-worker

Run each service in its own process. cctv-clip-worker works without MQTT (sweep-only); set CCTV_MQTT_HOST to receive reviews immediately. cctv-uploader reports DISABLED and uploads nothing when CCTV_STORAGE_BUCKET is empty; a bucket with no key exits 78.

ffprobe must be on PATH for the clip worker. The container image includes it; a pip install does not install ffmpeg.

Configuration

There are two kinds of configuration, and they must not be mixed:

File Holds Secret?
cameras.yml Site name, timezone, cameras, clip policy, retention No — but ${VAR} values come from the environment
environment / .env RTSP URLs, B2 keys, paths, ports, intervals Yes

The full template is .env.example. The settings most consumers need:

Variable Default Meaning
CCTV_CONFIG_DIR /config Directory containing cameras.yml
CCTV_DATA_DIR /data Root for clips/ and the state database
CCTV_DB $CCTV_DATA_DIR/cctv.db SQLite state store
CCTV_FRIGATE_URL http://frigate:5000 Frigate API base URL. Port 5000 must stay on a private network
CCTV_MQTT_HOST unset Unset means sweep-only; no broker required
CCTV_SWEEP_INTERVAL_S 60 Worst-case delay before a missed review is noticed
CCTV_STORAGE_BUCKET empty Empty means cloud backup DISABLED
CCTV_STORAGE_KEY_ID empty B2 application key ID
CCTV_STORAGE_APPLICATION_KEY empty B2 application key
CCTV_STORAGE_REGION us-west-004 B2 region
CCTV_UPLOADER_HEALTH_PORT 8082 Uploader /health and /metrics
CCTV_CLEANUP_HEALTH_PORT 8083 Cleanup /health and /metrics
INTERNAL_API_TOKEN empty Enables the uploader admin endpoints when set

Keep .env out of version control. RTSP URLs and B2 keys are credentials.

Camera names must be lowercase kebab-case (front-door): they become directory names and cloud object keys.

What it produces

$CCTV_DATA_DIR/
├── clips/
│   └── <camera>/YYYY/MM/DD/HH-MM-SS.mp4
│       <camera>/YYYY/MM/DD/HH-MM-SS.mp4.json
└── cctv.db

The clips tree is rendered in the site timezone. The sidecar records the review id, event ids, requested boundaries, measured coverage, codecs, size and SHA-256. The cloud object key is <location>/clips/<path-inside-clips>.

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

Health endpoints

Service Endpoint Notes
cctv-uploader http://127.0.0.1:8082/health, /metrics DISABLED without a bucket
cctv-cleanup http://127.0.0.1:8083/health, /metrics Local retention state
Uploader admin /admin/queue, /admin/retry Mounted only when INTERNAL_API_TOKEN is set

Bind these to loopback. Nothing in the package needs an inbound port from the LAN.

Python API

The supported contract is the CLI, the environment variables, and the HTTP health endpoints. The importable modules are tested, but pre-1.0 names may change between minor versions.

from cctv.config import ConfigError, load_cameras_config

try:
    config = load_cameras_config("cameras.yml")
except ConfigError as exc:
    raise SystemExit(f"invalid camera config: {exc}")

print(config.site.location)
for name, camera in config.enabled_cameras().items():
    print(name, camera.display_name, camera.record.mode)

Storage is available behind the storage extra:

import os
from cctv.storage import S3StorageAdapter

adapter = S3StorageAdapter(
    bucket=os.environ["CCTV_STORAGE_BUCKET"],
    access_key_id=os.environ["CCTV_STORAGE_KEY_ID"],
    secret_access_key=os.environ["CCTV_STORAGE_APPLICATION_KEY"],
    region=os.environ.get("CCTV_STORAGE_REGION", "us-west-004"),
)

cctv.__version__ reports the installed package version.

Complete installation

For a full CCTV site — Frigate, MQTT, the worker, uploader and cleanup — use the Compose deployment in this repository:

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

The first run creates .env and config/cameras.yml; edit both and run it again. See docs/install.md for the operator procedure and CONTRIBUTING.md for how releases are built and delivered.

Compatibility

Python 3.12 or newer
Frigate 0.18 API and MQTT contract (verified)
Platforms Linux x86_64/arm64; macOS for development
Cloud backup Backblaze B2 through its S3-compatible endpoint; optional
Inbound network None required

Documentation

Full documentation: https://aravindmyd.github.io/cctv-stack/

License

Apache-2.0 — see LICENSE.

Metadata

Release files for cctv-stack 0.7.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.7.0
File Size Uploaded Provenance
cctv_stack-0.7.0.tar.gz 173.9 kB With provenance Details

Built distribution (wheel)

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

Total release size: 368.4 kB

Release files / cctv_stack-0.7.0.tar.gz

Download URL cctv_stack-0.7.0.tar.gz
Size 173.9 kB
Tags Source
SHA-256 checksum
How to use checksums
d0ac2d0ba9bcc4b9b4bc035146942c3c37d1f74ad2e3bbf7416cbc6878c01688
BLAKE2b-256 checksum
How to use checksums
33cc03594ba2de9ba5ed7105a0584df4059231280a41e0dcbee3c347b2c31db1
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 20, 2026.

Transparency log

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

Download URL cctv_stack-0.7.0-py3-none-any.whl
Size 194.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cbacec1980756db8681adf7f8f46153ca2ab93d0491fb8fa0f97ca6b6b4d528a
BLAKE2b-256 checksum
How to use checksums
9f5472142fcfc9b0b5a866b16a3152842a6aa1a9ef9c707a1d28974824617a10
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 20, 2026.

Transparency log

Security for cctv-stack 0.7.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.7.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 20, 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