← All Tutorials

ViciDial Recording Storage — NFS, S3 & Archival Strategies

Infrastructure & DevOps Intermediate 15 min read #64

Master efficient recording storage architectures for ViciDial and Asterisk systems, comparing NFS block storage, S3 cloud solutions, and long-term archival strategies with production-tested configurations.

Prerequisites

Before implementing the strategies in this tutorial, you should have:

This tutorial assumes CentOS 7/8 or Ubuntu 18.04+ operating systems. Commands should be adapted for your specific distribution.

Understanding ViciDial Recording Architecture

How ViciDial Records Calls

ViciDial uses Asterisk's native recording capabilities to capture calls. When a call is bridged between an agent and a customer, Asterisk writes audio data to disk in real-time. The recording process is controlled by the dialplan and ViciDial application logic.

Call recordings are stored in raw Asterisk format (typically WAV or GSM) and are referenced in the vicidial_log table with the field recording_id. Each call record contains a unique identifier linking to its corresponding audio file.

Key recording-related fields in vicidial_log:

Storage Sizing Calculations

For production planning, use these baseline figures:

For a 100-agent center with average call length of 8 minutes:

Daily recordings = 100 agents × 80 calls/day × 480 seconds × 11 KB/sec
                 = 100 × 80 × 480 × 11 KB = 42,240,000 KB ≈ 40 GB/day
Monthly storage = 40 GB × 30 = 1.2 TB
Annual storage = 1.2 TB × 12 = 14.4 TB

This calculation shows why storage strategy is critical for scaling operations.

Recording Path Configuration in ViciDial

Default Recording Directory Structure

ViciDial records calls to /var/spool/asterisk/monitor/ by default. This directory is defined in the dialplan and should match your storage backend.

Verify current recording path in your extensions-vicidial.conf:

grep -n "monitor\|recording" /etc/asterisk/extensions-vicidial.conf | head -20

Standard ViciDial recording command in the dialplan:

exten => 8600,1,MixMonitor(${MIXMON_FILENAME}${MIXMON_FORMAT},b)
exten => 8600,n,Return()

Configuring Custom Recording Paths

To use a custom path, modify your ViciDial configuration. Edit the system settings table:

UPDATE vicidial_system_settings 
SET recording_directory = '/data/recordings' 
WHERE setting_name = 'asterisk_home';

Then verify Asterisk has permissions:

sudo chown -R asterisk:asterisk /data/recordings
sudo chmod 755 /data/recordings
ls -ld /data/recordings

Create subdirectories by date for better organization:

#!/bin/bash
# Create daily recording directories
for i in {0..30}; do
  date_dir=$(date -d "-$i days" +%Y/%m/%d)
  mkdir -p /data/recordings/$date_dir
  chown asterisk:asterisk /data/recordings/$date_dir
done

NFS-Based Recording Storage

When to Use NFS

NFS (Network File System) is ideal when:

NFS is not recommended for:

NFS Server Setup

Install NFS on your storage server:

# CentOS/RHEL
sudo yum install nfs-utils
sudo systemctl enable nfs-server
sudo systemctl start nfs-server

# Ubuntu/Debian
sudo apt-get install nfs-kernel-server
sudo systemctl enable nfs-server
sudo systemctl start nfs-server

Create the recording directory:

sudo mkdir -p /mnt/recordings
sudo chown nobody:nogroup /mnt/recordings
sudo chmod 755 /mnt/recordings

Configure NFS exports in /etc/exports:

/mnt/recordings 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash,atime=off)

The parameters explained:

Apply the exports:

sudo exportfs -a
sudo exportfs -v

NFS Client Configuration on Asterisk Servers

Install NFS client:

# CentOS/RHEL
sudo yum install nfs-utils

# Ubuntu/Debian
sudo apt-get install nfs-common

Create mount point:

sudo mkdir -p /var/spool/asterisk/monitor

Mount the NFS share:

sudo mount -t nfs -o rw,sync,hard,intr,rsize=8192,wsize=8192 \
  192.168.1.50:/mnt/recordings /var/spool/asterisk/monitor

Mount options explained:

Verify the mount:

mount | grep monitor
df -h /var/spool/asterisk/monitor

Make the mount persistent by adding to /etc/fstab:

192.168.1.50:/mnt/recordings /var/spool/asterisk/monitor nfs \
rw,sync,hard,intr,rsize=8192,wsize=8192,_netdev 0 0

Use _netdev to ensure NFS mounts after network is up.

NFS Performance Tuning

Monitor NFS performance:

# Install nfs-utils if not present
sudo yum install nfs-utils

# Check NFS statistics
nfsstat -c | head -20

# Monitor NFS operations per second
iostat -x 1 5 | grep nfs

For high-volume recording scenarios, increase buffer sizes on both server and client:

On NFS server (/etc/nfs.conf or kernel parameters):

# Edit /etc/sysctl.conf on the NFS server
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864

# Apply settings
sudo sysctl -p

On NFS client mount options, use larger buffers:

sudo mount -t nfs -o rw,sync,hard,intr,rsize=32768,wsize=32768 \
  192.168.1.50:/mnt/recordings /var/spool/asterisk/monitor

Test throughput with dd:

# Write test
dd if=/dev/zero of=/var/spool/asterisk/monitor/test-write bs=1M count=1000 oflag=direct
# Expected: 100+ MB/s on gigabit Ethernet

# Read test
dd if=/var/spool/asterisk/monitor/test-write of=/dev/null bs=1M
rm /var/spool/asterisk/monitor/test-write

S3-Based Recording Storage

Why S3 for ViciDial

Amazon S3 or compatible services (MinIO, DigitalOcean Spaces, Wasabi) offer:

Trade-offs:

S3 Bucket Configuration

Create an S3 bucket for ViciDial recordings:

# Using AWS CLI v2
aws s3 mb s3://vicidial-recordings-prod --region us-east-1

# Enable versioning for protection against accidental deletes
aws s3api put-bucket-versioning \
  --bucket vicidial-recordings-prod \
  --versioning-configuration Status=Enabled

# Set lifecycle policy to archive old recordings
aws s3api put-bucket-lifecycle-configuration \
  --bucket vicidial-recordings-prod \
  --lifecycle-configuration file://lifecycle.json

Create lifecycle.json:

{
  "Rules": [
    {
      "Id": "Archive old recordings to Glacier",
      "Status": "Enabled",
      "Prefix": "recordings/",
      "Transitions": [
        {
          "Days": 90,
          "StorageClass": "GLACIER"
        }
      ],
      "Expiration": {
        "Days": 2555
      }
    }
  ]
}

This policy:

Using S3fuse or S3FS

S3FS mounts S3 as a filesystem. Install on Asterisk server:

# CentOS/RHEL
sudo yum install epel-release
sudo yum install s3fs-fuse

# Ubuntu/Debian
sudo apt-get install s3fs

Create credentials file:

# Store AWS credentials (never commit to version control)
echo "AKIAIOSFODNN7EXAMPLE:wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" > ~/.s3fs-credentials
chmod 600 ~/.s3fs-credentials

Mount S3 bucket:

# Create mount point
sudo mkdir -p /var/spool/asterisk/monitor

# Mount S3 bucket
s3fs vicidial-recordings-prod /var/spool/asterisk/monitor \
  -o passwd_file=~/.s3fs-credentials \
  -o use_cache=/tmp \
  -o allow_other \
  -o mp_umask=002 \
  -o multithreaded \
  -o parallel_count=20 \
  -o multipart_size=64

Parameters explained:

Verify mount:

mount | grep s3fs
df -h /var/spool/asterisk/monitor
echo "test" > /var/spool/asterisk/monitor/test.txt
aws s3 ls s3://vicidial-recordings-prod/test.txt

Warning: S3FS has latency. For active recording paths, use local storage + asynchronous upload instead.

Recommended Approach: Local + S3 Upload

Use local storage for recording, then upload asynchronously to S3:

#!/bin/bash
# /usr/local/sbin/upload-recordings-to-s3.sh
# Run via cron every 5 minutes

RECORDING_DIR="/var/spool/asterisk/monitor"
S3_BUCKET="s3://vicidial-recordings-prod"
AWS_PROFILE="vicidial-prod"
LOG_FILE="/var/log/s3-upload.log"

# Find recordings older than 1 minute (completed calls)
find $RECORDING_DIR -name "*.wav" -type f -mmin +1 | while read recording; do
  # Skip if already uploaded (check for .uploaded marker)
  if [[ -f "${recording}.uploaded" ]]; then
    continue
  fi
  
  # Determine S3 path based on date
  file_date=$(stat -c %y "$recording" | cut -d' ' -f1 | tr '-' '/')
  s3_path="${S3_BUCKET}/recordings/${file_date}/$(basename $recording)"
  
  # Upload to S3
  if aws s3 cp "$recording" "$s3_path" --profile=$AWS_PROFILE >> "$LOG_FILE" 2>&1; then
    # Mark as uploaded
    touch "${recording}.uploaded"
    
    # Optionally delete local copy after 7 days
    find $RECORDING_DIR -name "*.uploaded" -mtime +7 | while read marker; do
      rm "${marker%.*}" "$marker"
    done
  fi
done

echo "[$(date)] Upload cycle complete" >> "$LOG_FILE"

Add to crontab:

*/5 * * * * /usr/local/sbin/upload-recordings-to-s3.sh

This approach:

S3-Compatible Services (MinIO)

For on-premises S3 storage, use MinIO:

# Install MinIO on a dedicated server
wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
sudo mv minio /usr/local/bin/

# Create data directory
sudo mkdir -p /mnt/minio-data
sudo chown minio:minio /mnt/minio-data

# Start MinIO
MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=securepassword \
  /usr/local/bin/minio server /mnt/minio-data

Access MinIO console at http://minio-server:9001 and configure like S3.

Recording Archival and Retention Strategies

Database-Driven Archival

Track recordings in the database and implement retention policies programmatically.

Query active recordings:

SELECT 
  call_id,
  recording_id,
  call_date,
  status,
  LENGTH(call_date) as duration_days,
  TIMESTAMPDIFF(DAY, call_date, NOW()) as age_days
FROM vicidial_log
WHERE recording_id IS NOT NULL
  AND recording_id != ''
  AND call_date > DATE_SUB(NOW(), INTERVAL 90 DAY)
ORDER BY call_date DESC
LIMIT 100;

Create a retention policy based on status:

-- Archive recordings for closed campaigns after 30 days
CREATE PROCEDURE archive_old_recordings()
BEGIN
  DECLARE done INT DEFAULT FALSE;
  DECLARE rec_id VARCHAR(100);
  DECLARE cur CURSOR FOR
    SELECT DISTINCT recording_id
    FROM vicidial_log
    WHERE recording_id IS NOT NULL
      AND recording_id != ''
      AND TIMESTAMPDIFF(DAY, call_date, NOW()) > 30
      AND campaign_id IN (SELECT campaign_id FROM vicidial_campaigns WHERE active='N');
  
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
  
  OPEN cur;
  
  read_loop: LOOP
    FETCH cur INTO rec_id;
    IF done THEN
      LEAVE read_loop;
    END IF;
    
    -- Log archival action
    INSERT INTO vicidial_log_archive (recording_id, archived_date, archive_location)
    VALUES (rec_id, NOW(), 'S3');
    
  END LOOP;
  
  CLOSE cur;
END;

Execute archival:

mysql -u vicidialuser -p asterisk -e "CALL archive_old_recordings();"

Implementing Deletion Policies

Create a safe deletion script that confirms recordings still exist before removal:

#!/bin/bash
# /usr/local/sbin/purge-old-recordings.sh
# Delete recordings older than retention period

RETENTION_DAYS=365
RECORDING_DIR="/var/spool/asterisk/monitor"
DB_USER="vicidialuser"
DB_PASS="PASSWORD"
DB_NAME="asterisk"

# Find recordings older than retention period
find $RECORDING_DIR -name "*.wav" -type f -mtime +$RETENTION_DAYS | while read recording; do
  filename=$(basename "$recording")
  recording_id="${filename%.*}"
  
  # Verify in database that this recording should be deleted
  exists=$(mysql -u$DB_USER -p$DB_PASS -N -e \
    "SELECT COUNT(*) FROM vicidial_log WHERE recording_id='$recording_id' LIMIT 1;" $DB_NAME)
  
  if [[ $exists -eq 0 ]]; then
    # Safe to delete
    echo "Deleting: $recording (age: $(stat -c %y $recording | cut -d' ' -f1))"
    rm "$recording"
  fi
done

Schedule in cron:

# Run daily at 2 AM
0 2 * * * /usr/local/sbin/purge-old-recordings.sh >> /var/log/purge-recordings.log 2>&1

Compression and Reencoding

For cost optimization, compress older recordings:

#!/bin/bash
# Compress recordings older than 7 days to MP3

DAYS_OLD=7
RECORDING_DIR="/var/spool/asterisk/monitor"

find $RECORDING_DIR -name "*.wav" -type f -mtime +$DAYS_OLD | while read wav_file; do
  mp3_file="${wav_file%.wav}.mp3"
  
  # Skip if already compressed
  [[ -f "$mp3_file" ]] && continue
  
  # Compress using ffmpeg at 64kbps (acceptable quality for voice)
  ffmpeg -i "$wav_file" -acodec libmp3lame -ab 64k -y "$mp3_file" 2>/dev/null
  
  if [[ $? -eq 0 ]]; then
    # Calculate space saved
    wav_size=$(stat -c%s "$wav_file")
    mp3_size=$(stat -c%s "$mp3_file")
    saved=$((wav_size - mp3_size))
    
    echo "Compressed $(basename $wav_file): saved ${saved} bytes"
    
    # Remove WAV after successful compression
    rm "$wav_file"
  fi
done

Typical compression results:

For best results with ViciDial, keep WAV for recent recordings (fast playback), compress to MP3 at 64kbps for 7-30 days, then archive to Glacier.

Archival Verification

Periodically verify archived recordings are recoverable:

#!/bin/bash
# Test recovery of random archived recordings

S3_BUCKET="s3://vicidial-recordings-prod"
SAMPLE_SIZE=10

# Get random sample of archived recordings
aws s3 ls $S3_BUCKET/recordings/ --recursive \
  | awk '{print $NF}' \
  | shuf -n $SAMPLE_SIZE | while read recording; do
  
  echo "Testing recovery of: $recording"
  
  # Download to temp location
  if aws s3 cp "$S3_BUCKET/$recording" /tmp/test-recovery.wav; then
    filesize=$(stat -c%s /tmp/test-recovery.wav)
    echo "✓ Successfully recovered: $recording ($filesize bytes)"
    rm /tmp/test-recovery.wav
  else
    echo "✗ FAILED to recover: $recording"
  fi
done

Monitoring and Troubleshooting

Monitoring Recording Storage

Create a monitoring script:

#!/bin/bash
# /usr/local/sbin/monitor-recording-storage.sh

RECORDING_DIR="/var/spool/asterisk/monitor"
WARNING_THRESHOLD=80  # percent full
CRITICAL_THRESHOLD=90

# Get usage percentage
usage=$(df $RECORDING_DIR | tail -1 | awk '{print $5}' | sed 's/%//')

# Count recordings by age
recent=$(find $RECORDING_DIR -name "*.wav" -type f -mtime -1 | wc -l)
week_old=$(find $RECORDING_DIR -name "*.wav" -type f -mtime +7 | wc -l)

# Size statistics
total_size=$(du -sh $RECORDING_DIR | cut -f1)

# Output metrics
echo "Recording Storage Status:"
echo "  Total: $total_size"
echo "  Usage: ${usage}%"
echo "  Recent recordings (< 1 day): $recent"
echo "  Old recordings (> 7 days): $week_old"

# Alert if threshold exceeded
if [[ $usage -gt $CRITICAL_THRESHOLD ]]; then
  echo "CRITICAL: Recording storage is $usage% full!"
  # Send alert (email, Slack, etc.)
  exit 2
elif [[ $usage -gt $WARNING_THRESHOLD ]]; then
  echo "WARNING: Recording storage is $usage% full"
  exit 1
fi

exit 0

Add to Nagios/Icinga monitoring:

# Run every 5 minutes
*/5 * * * * /usr/local/sbin/monitor-recording-storage.sh

NFS Troubleshooting

Problem: NFS mount hangs

# Check NFS server connectivity
showmount -e 192.168.1.50

# Force unmount if hung
sudo umount -f /var/spool/asterisk/monitor

# Remount with different options
sudo mount -t nfs -o soft,intr,retrans=3,timeo=15 \
  192.168.1.50:/mnt/recordings /var/spool/asterisk/monitor

Problem: Slow NFS performance

# Check client-side NFS operations
nfsstat -c

# Increase network buffers
sudo sysctl -w net.core.rmem_max=134217728
sudo sysctl -w net.core.wmem_max=134217728

# Monitor packet loss (run on client)
ping -c 100 192.168.1.50 | grep "packet loss"

Problem: Permission denied writing to NFS mount

# Verify Asterisk user can write
sudo -u asterisk touch /var/spool/asterisk/monitor/test.wav

# Check ownership and permissions
ls -ld /var/spool/asterisk/monitor
ls -ld /mnt/recordings  # on NFS server

# Fix permissions
sudo chown -R asterisk:asterisk /var/spool/asterisk/monitor
sudo chmod 755 /var/spool/asterisk/monitor

S3 Troubleshooting

Problem: S3 uploads failing

# Check AWS credentials
aws sts get-caller-identity

# Test S3 bucket access
aws s3 ls s3://vicidial-recordings-prod/

# Check IAM permissions (need s3:PutObject, s3:GetObject, s3:ListBucket)
aws iam get-user-policy --user-name vicidial-s3-user --policy-name vicidial-s3-policy

Problem: S3FS mount unreliable

# Check S3FS logs
tail -f /var/log/syslog | grep s3fs

# Unmount and remount with diagnostics
sudo fusermount -u /var/spool/asterisk/monitor
s3fs -d -f vicidial-recordings-prod /var/spool/asterisk/monitor \
  -o passwd_file=~/.s3fs-credentials 2>&1 | tee /tmp/s3fs-debug.log

Asterisk CLI Monitoring

Monitor recordings in real-time via Asterisk CLI:

# Connect to Asterisk console
sudo asterisk -rx "core show channels verbose"

# Check MixMonitor status
sudo asterisk -rx "mixmonitor show"

# List active recordings
sudo asterisk -rx "mixmonitor list"

# Stop a specific recording (if needed)
sudo asterisk -rx "mixmonitor stop <call_id>"

View Asterisk recording logs:

# Follow Asterisk message log
tail -f /var/log/asterisk/messages | grep -i "mixmonitor\|recording"

Summary

Effective ViciDial recording storage requires matching your architecture to operational needs:

Use NFS when:

Use S3 when:

Best practice hybrid approach:

Key metrics to track:

Implement monitoring with appropriate alerting, test failover scenarios regularly, and review retention policies annually as call volume and compliance requirements evolve. The most sophisticated storage system is worthless if recordings cannot be recovered when needed.

Stuck on something specific?

Book a free 30-minute call. I run ViciDial centers across 3 countries and can usually unblock your setup in one session — or build it for you.

Book a Free Consultation