I've been tinkering with IP cameras at home for years. What started as a single camera monitoring the driveway turned into a multi-camera system, and with it came the realization that commercial solutions were either too expensive, too limited, or required subscriptions that felt unnecessary. I wanted something simple: capture video from my cameras continuously, store it efficiently, and make it available for whatever I needed—whether that was viewing in HomeKit, running AI detection, or just reviewing footage when the neighbor's cat set off the motion sensor again.
tapes is the result of that journey. It's built around a straightforward idea: use proven open-source tools to capture RTSP streams from IP cameras, save continuous recordings in manageable interval files, and provide a foundation for everything else. The recordings become source material for AI-based event detection, live viewing through HomeKit, or simply browsing through footage when you need to see what happened last Tuesday afternoon.
This isn't trying to be a complete commercial NVR replacement with every feature imaginable. Instead, it's a solid foundation that handles the core job well—continuous recording with a clean web interface—and stays out of your way when you want to build on top of it. The system has been running in production for months, quietly doing its job while I've refined the pieces that matter.
Recent improvements have simplified the architecture significantly: MediaMTX now handles recording natively using crash-resilient fragmented MP4 files, automatic retention management keeps disk usage in check, and structured logging makes debugging straightforward. The result is a cleaner, more reliable system that just works.
tapes is built on three core components that work together:
- mediamtx proxy: Proxies your IP camera RTSP streams and handles native continuous recording
- web application: Go backend with React frontend for browsing and viewing recordings and live video
- disk storage: Simple file-based storage with automatic thumbnail generation and retention management
Everything runs in Docker containers orchestrated by docker-compose for easy deployment and management.
Before you begin, you'll need:
- A Linux server (Ubuntu/Debian recommended) with Docker
- One or more IP cameras with RTSP streams
Create the directory structure for your camera recordings:
sudo mkdir -p /path/to/cameras/{garage,kitchen}Replace garage and kitchen with your actual camera names. The structure will look like:
/path/to/cameras/
├── garage/
└── kitchen/
MediaMTX will create video files directly in each camera directory—no subdirectories needed.
Create the installation directory and download required files:
mkdir -p ~/tapes
cd ~/tapes
# Download docker-compose configuration
curl -o docker-compose.yml https://raw.githubusercontent.com/vicgarcia/tapes/main/docker-compose.yml
# Download environment template
curl -o .env.template https://raw.githubusercontent.com/vicgarcia/tapes/main/.env.template
# Download MediaMTX configuration
curl -o mediamtx.yml https://raw.githubusercontent.com/vicgarcia/tapes/main/mediamtx.ymlCreate your environment configuration:
cp .env.template .env
nano .envEdit the .env file with your settings:
# Required
JWT_KEY=your-secret-key-here # Generate with: openssl rand -hex 32
# Optional (defaults shown)
STORAGE_PATH=/path/to/cameras # Path to camera recordings
PASSWORDS_PATH=/path/to/passwords # Path to htpasswd file
JWT_ISSUER=tapes # JWT issuer identifier
DEBUG=false # Enable debug logging (true/false)
RETENTION_DAYS=90 # Auto-delete after 90 days (0=keep forever)Important: Generate a secure JWT_KEY with openssl rand -hex 32 - don't use a simple password.
Create a password file for web interface access:
cd ~/tapes
htpasswd -c passwords admin # Create file with first user
htpasswd passwords viewer # Add additional usersEdit the MediaMTX configuration at mediamtx.yml:
paths:
garage:
source: rtsp://user:password@192.168.1.101:554/live/ch0
sourceProtocol: tcp
kitchen:
source: rtsp://user:password@192.168.1.102:554/live/ch0
sourceProtocol: tcpThat's it! MediaMTX handles recording automatically using the global defaults already configured in mediamtx.yml:
- Native recording: Built-in recording (no FFmpeg needed)
- Format: Fragmented MP4 (fmp4) - resilient to crashes
- Segment duration: 5-minute video files
- Part duration: 1-second disk flush (max 1s data loss on crash)
- Retention: Auto-delete after 90 days (configurable)
- Filename format:
YYYYMMDDHHMMSS.mp4(e.g.,20251109143000.mp4)
Simply add your camera's RTSP URL and MediaMTX handles the rest.
Edit docker-compose.yml to match your camera storage path and passwords file location:
nano docker-compose.ymlUpdate the volume paths in both the mediamtx and tapes services:
services:
mediamtx:
volumes:
- ./mediamtx.yml:/mediamtx.yml:ro
- /path/to/cameras:/cameras # Update this path
tapes:
volumes:
- /path/to/cameras:/cameras # Update this path
- ~/tapes/passwords:/opt/tapes/passwords:roReplace /path/to/cameras with the actual path you created in step 1.
Launch both MediaMTX and tapes with docker-compose:
docker-compose up -dCheck that everything is running:
docker-compose ps
docker-compose logsThe web application will be available at http://<server ip>:8671.
The pre-built tapes image will be automatically pulled from GitHub Container Registry (ghcr.io), saving you 10-15 minutes of build time with OpenCV.
Once running, open http://<server ip>:8671 in your browser:
- Login with credentials from your htpasswd file
- Select Camera from the dropdown
- Choose Date to browse recordings
- Click thumbnails to play videos
The interface automatically generates thumbnails for all videos hourly. Recordings are played as native H.264 video in your browser. Videos are stored in 5-minute segments, making it easy to find exactly what you need.
Monitor disk usage regularly:
df -h /path/to/camerasWith 5-minute segments, storage usage depends on your bitrate. Typical cameras at 2-4 Mbps use roughly:
- Per camera: 1-2 GB per day, 30-60 GB per month
- Three cameras: 3-6 GB per day, 90-180 GB per month
By default, tapes automatically deletes recordings older than 90 days. You can adjust this in .env:
RETENTION_DAYS=90 # Auto-delete after 90 days
RETENTION_DAYS=0 # Keep forever (disable auto-deletion)Note that MediaMTX also has its own retention setting in mediamtx.yml (recordDeleteAfter). Both can run independently—set them to match for consistency.
For SSL and domain access, use nginx:
server {
listen 443 ssl;
server_name cameras.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:8671;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}- Recordings:
YYYYMMDDHHMMSS.mp4(timestamp when segment started) - Thumbnails:
.jpgfiles with matching names
All endpoints require JWT authentication:
POST /login- Authenticate with htpasswd credentialsGET /cameras- List available camerasGET /cameras/{camera}/recordings?day=YYYY-MM-DD- Get recordings for dateGET /cameras/{camera}/recordings/{timestamp}/video- Stream recordingGET /cameras/{camera}/recordings/{timestamp}/thumbnail- Get thumbnail
tapes runs an hourly background task that:
- Scans all camera directories for video files
- Generates thumbnails for any videos missing them using OpenCV
- Skips the most recent file (may still be recording)
- Deletes videos older than RETENTION_DAYS (default: 90 days)
- Removes zero-byte files (corrupted recordings)
All processing is logged with structured output for easy monitoring.
If you want to modify the code, clone the repository:
git clone https://github.com/vicgarcia/tapes.git
cd tapesTo build the Docker image locally for development:
docker build -t ghcr.io/vicgarcia/tapes:local .This creates a local image with the tag local that docker-compose will use instead of pulling from the registry. The build takes 10-15 minutes due to OpenCV compilation.
# Frontend development
cd ui && npm run dev
# Backend development
cd api && go run .
# Code formatting
npm run lint && go fmt ./...