32blogby StudioMitsu

Construye un servidor de codificación FFmpeg en un VPS

Configura un servidor dedicado de codificación FFmpeg en un VPS. Cubre instalación, automatización con carpeta de vigilancia, servicio systemd y una API REST simple.

13 min read

This article contains affiliate links.

Contenido

Si codificas video en tu máquina local, tu CPU se satura y no puedes hacer nada más durante la siguiente hora. Todos hemos pasado por eso — la máquina se arrastra mientras FFmpeg procesa el material.

Este artículo te guía paso a paso para construir un servidor dedicado de codificación FFmpeg en un VPS. Al final tendrás una carpeta de vigilancia que activa la codificación automáticamente, un servicio systemd que lo mantiene funcionando 24/7 y una API REST simple para enviar trabajos de forma remota.

Lo que aprenderás

  • Por qué tiene sentido delegar la codificación a un VPS
  • Cómo elegir las especificaciones adecuadas del VPS
  • Instalación y configuración de FFmpeg desde cero
  • Construcción de una carpeta de vigilancia con inotifywait
  • Ejecución del vigilante como servicio systemd
  • Exposición de una API REST mínima con FastAPI
  • Consejos de producción: gestión de disco, límites de recursos, registros

Por qué ejecutar la codificación en un VPS

El principal problema con la codificación local no es solo la carga de CPU — es la fiabilidad. Deja una codificación nocturna corriendo en un portátil y hay una buena probabilidad de que se haya despertado del modo suspensión a mitad del proceso, o de que el ventilador haya hecho tanto ruido que mataste el proceso.

Un VPS resuelve esto con tres beneficios concretos.

Tu máquina local queda libre. Una vez que subes el archivo, la codificación se ejecuta completamente en el VPS. Puedes cerrar tu portátil, cambiar de contexto y volver a un archivo terminado.

Tiempo de actividad garantizado. Las instancias VPS corren en centros de datos sin suspensión, sin cortes de energía, sin apagados accidentales. Las codificaciones largas terminan de forma fiable.

Escalado elástico. Cuando la capacidad no es suficiente, actualiza el VPS o levanta un segundo. Eso es más barato y rápido que comprar una nueva máquina local.

En cuanto al costo, un VPS con suficiente CPU para trabajo de video típico cuesta $3–15/mes dependiendo de la región y el proveedor. Mucho menos que una estación de trabajo dedicada para codificación.

Elección del VPS y estimación de especificaciones

La cantidad de núcleos de CPU es la especificación más importante para FFmpeg. Usa todos los hilos disponibles para la codificación por software, así que más núcleos significa directamente codificaciones más rápidas.

Guía de especificaciones

Caso de usoCPURAMAlmacenamiento
Pruebas / personal2 núcleos2 GB50 GB SSD
Escala media (varios archivos/día)4 núcleos4 GB100 GB SSD
Producción8+ núcleos8+ GB200+ GB SSD

La codificación por GPU (NVENC, VAAPI) es significativamente más rápida pero requiere un plan con GPU habilitada, que cuesta considerablemente más. Para la mayoría de los flujos de trabajo, libx264 o libx265 en CPU entrega excelente calidad sin el sobreprecio.

Proveedores recomendados

  • DigitalOcean — precios directos, buena documentación, fácil de redimensionar
  • Hetzner — excelente relación precio-rendimiento en Europa
  • Vultr — precios competitivos a nivel global, facturación por hora

Elige Ubuntu 22.04 LTS como sistema operativo. Soporte a largo plazo hasta 2027, documentación extensa, y la mayoría de las guías de FFmpeg lo tienen como objetivo.

Instalación y configuración de FFmpeg

Conéctate por SSH a tu VPS y comienza con una actualización del sistema.

bash
# Update the system
sudo apt update && sudo apt upgrade -y

# Install FFmpeg and required tools
sudo apt install -y ffmpeg inotify-tools python3-pip python3-venv

# Verify the install
ffmpeg -version

Deberías ver ffmpeg version 6.x o similar. Si el comando no se encuentra, la instalación con apt falló — vuelve a ejecutar con sudo apt install -y ffmpeg.

Configura la estructura de directorios.

bash
# Create the encoding server directories
sudo mkdir -p /opt/encoder/{watch,processing,done,failed,logs}

# Create a dedicated system user (never run as root)
sudo useradd -r -s /bin/false encoder

# Transfer ownership
sudo chown -R encoder:encoder /opt/encoder

Roles de los directorios:

  • watch/ — deja archivos aquí para activar la codificación
  • processing/ — los archivos se mueven aquí mientras la codificación se ejecuta
  • done/ — las codificaciones completadas aterrizan aquí
  • failed/ — los archivos con errores aterrizan aquí
  • logs/ — archivos de registro para el vigilante y FFmpeg

A continuación, escribe el script de codificación que hace la transcodificación real.

bash
# Create /opt/encoder/encode.sh
sudo tee /opt/encoder/encode.sh > /dev/null << 'EOF'
#!/bin/bash
set -euo pipefail

INPUT="$1"
BASENAME=$(basename "$INPUT" | sed 's/\.[^.]*$//')
OUTPUT="/opt/encoder/done/${BASENAME}_encoded.mp4"

ffmpeg -i "$INPUT" \
  -c:v libx264 \
  -preset slow \
  -crf 23 \
  -c:a aac \
  -b:a 128k \
  -movflags +faststart \
  "$OUTPUT" \
  2>> /opt/encoder/logs/ffmpeg.log

echo "Done: $OUTPUT"
EOF

sudo chmod +x /opt/encoder/encode.sh
sudo chown encoder:encoder /opt/encoder/encode.sh

-crf 23 es un buen punto de partida para el balance calidad/tamaño. Valores más bajos (18–20) dan mayor calidad con archivos más grandes. Valores más altos (26–28) comprimen más agresivamente. El punto óptimo para la mayoría del contenido es 20–26.

Automatización de la codificación con una carpeta de vigilancia

El script de vigilancia escucha nuevos archivos en watch/ y ejecuta el script de codificación en cada uno.

bash
# Create /opt/encoder/watch.sh
sudo tee /opt/encoder/watch.sh > /dev/null << 'EOF'
#!/bin/bash
set -euo pipefail

WATCH_DIR="/opt/encoder/watch"
PROCESSING_DIR="/opt/encoder/processing"
FAILED_DIR="/opt/encoder/failed"
LOG="/opt/encoder/logs/watch.log"

log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"
}

log "Encoder watch started. Watching: $WATCH_DIR"

inotifywait -m -e close_write --format '%f' "$WATCH_DIR" | while read -r FILENAME; do
  FILEPATH="${WATCH_DIR}/${FILENAME}"

  # Only process supported video formats
  EXT="${FILENAME##*.}"
  EXT_LOWER=$(echo "$EXT" | tr '[:upper:]' '[:lower:]')
  if [[ ! "$EXT_LOWER" =~ ^(mp4|mkv|mov|avi)$ ]]; then
    log "Skipped (unsupported format): $FILENAME"
    continue
  fi

  log "Detected: $FILENAME"

  # Move to processing to prevent double-triggering
  PROC_PATH="${PROCESSING_DIR}/${FILENAME}"
  mv "$FILEPATH" "$PROC_PATH"
  log "Moved to processing: $FILENAME"

  # Run the encode
  if /opt/encoder/encode.sh "$PROC_PATH"; then
    rm -f "$PROC_PATH"
    log "Success: $FILENAME"
  else
    mv "$PROC_PATH" "${FAILED_DIR}/${FILENAME}"
    log "Failed: $FILENAME — moved to failed/"
  fi
done
EOF

sudo chmod +x /opt/encoder/watch.sh
sudo chown encoder:encoder /opt/encoder/watch.sh

Pruébalo manualmente antes de conectarlo a systemd.

bash
# Start the watcher in the background
sudo -u encoder /opt/encoder/watch.sh &

# Drop a test file into the watch folder
cp /path/to/test.mp4 /opt/encoder/watch/

# Tail the log
tail -f /opt/encoder/logs/watch.log

Ejecución del vigilante como servicio systemd

Un script corriendo en segundo plano no es apto para producción. Regístralo como un servicio systemd para que se inicie en el arranque y se reinicie automáticamente si falla.

ini
# /etc/systemd/system/encoder.service
[Unit]
Description=FFmpeg Encoding Watch Service
After=network.target

[Service]
Type=simple
User=encoder
Group=encoder
ExecStart=/opt/encoder/watch.sh
Restart=on-failure
RestartSec=5s
StandardOutput=append:/opt/encoder/logs/systemd.log
StandardError=append:/opt/encoder/logs/systemd.log

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/opt/encoder
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Habilita e inicia el servicio.

bash
# Reload systemd to pick up the new unit file
sudo systemctl daemon-reload

# Enable auto-start on boot
sudo systemctl enable encoder.service

# Start it now
sudo systemctl start encoder.service

# Check status
sudo systemctl status encoder.service

Active: active (running) significa que está funcionando. Usa journalctl para una inspección más profunda de los registros.

bash
# Follow logs in real time
sudo journalctl -u encoder.service -f

# View logs from the last hour
sudo journalctl -u encoder.service --since "1 hour ago"

Agregar una API de envío de trabajos remota

SCP a la carpeta de vigilancia es fiable pero requiere acceso SSH. Una API REST te permite enviar trabajos desde scripts, pipelines de CI o cualquier cliente HTTP.

Instala FastAPI en un entorno virtual.

bash
# Create a Python virtual environment
python3 -m venv /opt/encoder/venv

# Install FastAPI and Uvicorn
/opt/encoder/venv/bin/pip install fastapi uvicorn python-multipart

Escribe el servidor API.

python
# /opt/encoder/api.py
import os
import shutil
from pathlib import Path

from fastapi import Depends, FastAPI, File, HTTPException, Security, UploadFile
from fastapi.responses import JSONResponse
from fastapi.security import APIKeyHeader

app = FastAPI(title="FFmpeg Encoding API")

API_KEY = os.environ.get("ENCODER_API_KEY", "")
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)

WATCH_DIR = Path("/opt/encoder/watch")
DONE_DIR = Path("/opt/encoder/done")
FAILED_DIR = Path("/opt/encoder/failed")
PROCESSING_DIR = Path("/opt/encoder/processing")


def verify_api_key(key: str = Security(api_key_header)) -> str:
    if not API_KEY:
        raise HTTPException(status_code=500, detail="API key not configured on server")
    if key != API_KEY:
        raise HTTPException(status_code=403, detail="Invalid API key")
    return key


@app.post("/encode")
async def submit_encode(
    file: UploadFile = File(...),
    _: str = Depends(verify_api_key),
) -> JSONResponse:
    """Upload a video file and queue it for encoding."""
    allowed_exts = {".mp4", ".mkv", ".mov", ".avi"}
    ext = Path(file.filename).suffix.lower()

    if ext not in allowed_exts:
        raise HTTPException(
            status_code=400,
            detail=f"Unsupported file type: {ext}. Allowed: {allowed_exts}",
        )

    dest = WATCH_DIR / file.filename
    with dest.open("wb") as f:
        shutil.copyfileobj(file.file, f)

    return JSONResponse(
        status_code=202,
        content={"status": "queued", "filename": file.filename},
    )


@app.get("/status")
async def get_status(_: str = Depends(verify_api_key)) -> JSONResponse:
    """Return file counts for each directory."""
    return JSONResponse(
        content={
            "watching": len(list(WATCH_DIR.iterdir())),
            "processing": len(list(PROCESSING_DIR.iterdir())),
            "done": len(list(DONE_DIR.iterdir())),
            "failed": len(list(FAILED_DIR.iterdir())),
        }
    )


@app.get("/health")
async def health_check() -> JSONResponse:
    """Health check endpoint — no auth required."""
    return JSONResponse(content={"status": "ok"})

Registra la API como servicio systemd.

bash
# Create the API service unit file
sudo tee /etc/systemd/system/encoder-api.service > /dev/null << 'EOF'
[Unit]
Description=FFmpeg Encoding API Server
After=network.target

[Service]
Type=simple
User=encoder
Group=encoder
WorkingDirectory=/opt/encoder
Environment="ENCODER_API_KEY=your-secret-key-here"
ExecStart=/opt/encoder/venv/bin/uvicorn api:app --host 127.0.0.1 --port 8000
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable encoder-api.service
sudo systemctl start encoder-api.service

Genera una clave API fuerte con openssl.

bash
# Generate a random 32-byte hex key
openssl rand -hex 32

Envía un trabajo via curl.

bash
# Upload a file for encoding
curl -X POST http://your-vps-ip:8000/encode \
  -H "X-API-Key: your-secret-key-here" \
  -F "file=@/path/to/video.mp4"

# Check queue status
curl http://your-vps-ip:8000/status \
  -H "X-API-Key: your-secret-key-here"

Para producción, pon Nginx al frente de la API como proxy inverso y agrega TLS. Enlazar Uvicorn a 127.0.0.1 significa que solo escucha en localhost — Nginx maneja la conexión HTTPS pública.

Consideraciones de producción

Gestión de espacio en disco

Los archivos de video llenan los discos rápidamente. Establece una política para la carpeta done/ antes de entrar en producción. El enfoque más simple es un cron job que elimina archivos con más de N días de antigüedad.

bash
# Delete files in done/ older than 7 days (add to crontab -e)
0 3 * * * find /opt/encoder/done -type f -mtime +7 -delete

Limitar codificaciones concurrentes

Si múltiples archivos llegan a la carpeta de vigilancia simultáneamente, el vigilante inicia codificaciones paralelas y satura la CPU. Agrega un archivo de bloqueo o usa flock en el script de codificación para forzar una codificación a la vez.

Baja la prioridad del proceso con nice para que el sistema operativo siga respondiendo incluso durante codificaciones pesadas.

bash
# In encode.sh — wrap the ffmpeg call with nice
nice -n 10 ffmpeg -i "$INPUT" ...

Rotación de registros

Los registros en /opt/encoder/logs/ crecen indefinidamente sin rotación. Configura logrotate.

bash
# Create /etc/logrotate.d/encoder
sudo tee /etc/logrotate.d/encoder > /dev/null << 'EOF'
/opt/encoder/logs/*.log {
    daily
    rotate 7
    compress
    missingok
    notifempty
}
EOF

Transferencia de archivos

SCP es la forma más simple de enviar archivos desde una máquina local a la carpeta de vigilancia.

bash
# Push a local file to the watch folder
scp /path/to/video.mp4 user@your-vps-ip:/opt/encoder/watch/

Configura autenticación con clave pública SSH para no necesitar escribir una contraseña en cada transferencia. Agrega un alias a ~/.ssh/config si haces esto frecuentemente.

Almacenamiento y distribución

Una vez que la codificación termina, necesitas un lugar para poner la salida. Elige el servicio según tu caso de uso.

Almacenamiento de objetos en la nube

ServicioPrecioMejor para
Backblaze B2$6/TB/mes (10 GB gratis)Almacenamiento a gran escala más barato
Google Drive15 GB gratisCompartir rápido con una cuenta de Google
Cloudflare R2~$0.015/GB/mes, sin cargos de egresoDistribución CDN sin costos de ancho de banda
Dropbox2 GB gratisColaboración en equipo

Para archivos de video grandes, Backblaze B2 o Cloudflare R2 ofrecen el mejor valor. Para compartir de forma casual, Google Drive está bien.

Plataformas de video

PlataformaTipoMejor para
YouTubeGratuito, ilimitado, alcance masivoContenido público, monetización
VimeoSin anuncios, streaming de alta calidadPortafolios, revisiones de clientes
Bunny.netCDN asequible + streaming de videoInsertar video en tu propio servicio

NAS auto-hospedado

Si quieres control total sobre tu biblioteca de video, un NAS Synology o QNAP funciona bien. Instala Jellyfin o Plex para transcodificación bajo demanda basada en FFmpeg y un servidor de medios doméstico completo.

Artículos relacionados:

Conclusión

Esto es lo que construimos y por qué funciona.

  • VPS en lugar de local: libera tu máquina, garantiza tiempo de actividad, escala sin cambios de hardware
  • Elección de especificaciones: 2 núcleos para uso personal, 8+ para producción
  • Instalación de FFmpeg: apt install ffmpeg inotify-tools cubre lo esencial
  • Carpeta de vigilancia: inotifywait -e close_write se activa solo después de que un archivo se ha escrito completamente — seguro para SCP
  • Servicio systemd: se inicia automáticamente en el arranque, se reinicia en fallos, registra en journald
  • API REST: FastAPI + autenticación por clave API para envío remoto de trabajos
  • Producción: cron de limpieza de disco, nice para cortesía de CPU, logrotate para higiene de registros

Comienza con la carpeta de vigilancia y el servicio systemd — ese es el núcleo del sistema y cubre la mayoría de los casos de uso. Agrega la API después si necesitas envío programático de trabajos. Cuando el VPS se quede sin capacidad, actualiza la instancia; el código no cambia.


¿Cansado de memorizar comandos de FFmpeg? Prueba ffmpeg-quick — una CLI de código abierto que envuelve tareas comunes como compresión, HLS y creación de GIF en presets simples que puedes ejecutar con npx.

さくらのVPS

Reliable Japanese VPS — great for encoding workloads

  • 2GB plan from ~$11/month
  • Tokyo, Osaka & Ishikari regions
  • SSD 100GB, 3 vCPU cores