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 uso | CPU | RAM | Almacenamiento |
|---|---|---|---|
| Pruebas / personal | 2 núcleos | 2 GB | 50 GB SSD |
| Escala media (varios archivos/día) | 4 núcleos | 4 GB | 100 GB SSD |
| Producción | 8+ núcleos | 8+ GB | 200+ 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.
# 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.
# 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ónprocessing/— los archivos se mueven aquí mientras la codificación se ejecutadone/— 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.
# 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.
# 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.
# 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.
# /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.
# 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.
# 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.
# 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.
# /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.
# 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.
# Generate a random 32-byte hex key
openssl rand -hex 32
Envía un trabajo via curl.
# 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.
# 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.
# 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.
# 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.
# 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
| Servicio | Precio | Mejor para |
|---|---|---|
| Backblaze B2 | $6/TB/mes (10 GB gratis) | Almacenamiento a gran escala más barato |
| Google Drive | 15 GB gratis | Compartir rápido con una cuenta de Google |
| Cloudflare R2 | ~$0.015/GB/mes, sin cargos de egreso | Distribución CDN sin costos de ancho de banda |
| Dropbox | 2 GB gratis | Colaboració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
| Plataforma | Tipo | Mejor para |
|---|---|---|
| YouTube | Gratuito, ilimitado, alcance masivo | Contenido público, monetización |
| Vimeo | Sin anuncios, streaming de alta calidad | Portafolios, revisiones de clientes |
| Bunny.net | CDN asequible + streaming de video | Insertar 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-toolscubre lo esencial - Carpeta de vigilancia:
inotifywait -e close_writese 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,
nicepara 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.
Reliable Japanese VPS — great for encoding workloads
- 2GB plan from ~$11/month
- Tokyo, Osaka & Ishikari regions
- SSD 100GB, 3 vCPU cores