Copia de seguridad y restauración de volúmenes Docker en un VPS
Tres estrategias de backup para volúmenes Docker en un VPS: snapshots con tar, dumps nativos de bases de datos y copias de seguridad automatizadas y cifradas con offen/docker-volume-backup. Incluye planificación con cron, copias remotas a S3 con rclone y una prueba de restauración completa en un servidor nuevo.
Los volúmenes con nombre de Docker contienen tus datos de producción: bases de datos, archivos subidos, estado de configuración. Los contenedores son desechables. Los volúmenes no. Si un disco falla o una migración sale mal, los volúmenes son lo que necesitas recuperar.
Esta guía cubre tres estrategias de backup, las automatiza con cron, envía copias fuera del servidor con rclone, y luego demuestra que la restauración funciona reconstruyendo todo en un VPS nuevo. Si no has probado tu restauración, no tienes backups.
Lo que vas a aprender:
- Snapshots de volúmenes con tar (simple, universal)
- Dumps nativos de bases de datos con
pg_dumpymysqldump(consistentes, sin tiempo de inactividad) - Backups automatizados y cifrados con offen/docker-volume-backup (programados, compatibles con S3)
- Automatización con cron y políticas de retención
- Copias remotas a almacenamiento compatible con S3 vía rclone
- Restauración completa ante desastres en un VPS nuevo
Requisitos previos
Necesitas un VPS con Debian 12 o Ubuntu 24.04 con Docker y Docker Compose v2 instalados. Esta guía asume que tienes un stack Compose en ejecución con al menos un volumen con nombre. Si necesitas configurar eso primero, consulta nuestra guía de Docker Compose multi-servicio en un VPS.
Verifica tu instalación:
docker --version
# Docker version 28.x or newer
docker compose version
# Docker Compose version v2.x or newer
Revisa tus volúmenes existentes:
docker volume ls
La salida lista cada volumen con nombre en el sistema. Identifica cuáles contienen datos que te importan. Usa docker system df -v para ver cuánto espacio usa cada volumen. Esto ayuda a estimar el tamaño de los backups y las necesidades de almacenamiento.
Crea un directorio de backup con permisos restringidos:
mkdir -p /opt/backups/docker
chmod 700 /opt/backups/docker
Solo root puede leer este directorio. Los backups suelen contener credenciales de bases de datos, tokens de sesión o datos de usuarios.
¿Cómo hago backup de volúmenes Docker en un VPS?
Hay tres estrategias, cada una con diferentes compromisos. Elige según el contenido de tus volúmenes y cuánto tiempo de inactividad puedes tolerar.
| Método | Inactividad | Consistencia de datos | Automatización | Cifrado | Ideal para |
|---|---|---|---|---|---|
| Snapshot tar | Breve (contenedor detenido) | Nivel de sistema de archivos | Manual o script cron | No (añadir GPG por separado) | Archivos estáticos, uploads, config |
| Dump de base de datos | Ninguna | Consistencia transaccional | Manual o script cron | No (añadir GPG por separado) | PostgreSQL, MySQL, MariaDB |
| offen/docker-volume-backup | Opcional (configurable) | Nivel de sistema de archivos | Scheduler integrado | GPG integrado | Cualquier volumen, operación autónoma |
¿Cómo creo un backup tar de un volumen Docker?
Detén el contenedor que usa el volumen, ejecuta un contenedor Alpine temporal para archivar el contenido del volumen con tar, y reinicia. Esto toma segundos para la mayoría de los volúmenes y funciona con cualquier tipo de datos.
1. Detén el contenedor que escribe en el volumen:
# Replace "app" with your service name from docker-compose.yml
docker compose stop app
Detener el escritor previene escrituras parciales durante el archivado. Para volúmenes de solo lectura o archivos estáticos, puedes saltarte este paso.
2. Crea el archivo tar:
docker run --rm \
-v myapp_data:/source:ro \
-v /opt/backups/docker:/backup \
alpine tar czf /backup/myapp_data-$(date +%Y%m%d-%H%M%S).tar.gz -C /source .
Qué hace esto: lanza un contenedor Alpine desechable, monta tu volumen como solo lectura en /source, monta el directorio de backup en /backup, y crea un archivo tar comprimido con gzip. El flag --rm elimina el contenedor cuando termina. El flag :ro previene que el proceso de backup escriba accidentalmente en tus datos.
3. Reinicia el contenedor:
docker compose start app
4. Verifica el archivo:
ls -lh /opt/backups/docker/myapp_data-*.tar.gz
La salida muestra el archivo con un tamaño razonable. Un volumen de 500 MB se comprime típicamente a 60-120 MB dependiendo del tipo de datos.
Lista el contenido del archivo para confirmar que los ficheros están ahí:
tar tzf /opt/backups/docker/myapp_data-20260319-120000.tar.gz | head -20
Fíjate bien: las rutas deben empezar con ./ (sin nombre de directorio inicial). Esto es porque usamos -C /source . en el comando tar. Esto importa durante la restauración.
¿Cómo hago backup de una base de datos PostgreSQL en Docker?
Usa pg_dump dentro del contenedor en ejecución. Esto produce un dump transaccionalmente consistente sin detener la base de datos. El formato custom (-Fc) comprime la salida y permite restauración selectiva.
docker compose exec -T postgres pg_dump \
-U "$POSTGRES_USER" \
-Fc \
--no-owner \
--no-acl \
mydb > /opt/backups/docker/mydb-$(date +%Y%m%d-%H%M%S).dump
Qué hace esto: exec -T ejecuta el comando dentro del contenedor postgres en ejecución sin asignar un TTY (necesario para redirigir la salida). -Fc selecciona el formato custom, que está comprimido y es compatible con pg_restore. --no-owner y --no-acl hacen el dump portable entre diferentes usuarios de base de datos.
La variable $POSTGRES_USER debe venir de tu archivo de entorno, no estar escrita directamente en el código. Si tu stack Compose usa un archivo env:
source /opt/myapp/.env
docker compose exec -T postgres pg_dump \
-U "$POSTGRES_USER" \
-Fc \
--no-owner \
--no-acl \
"$POSTGRES_DB" > /opt/backups/docker/"$POSTGRES_DB"-$(date +%Y%m%d-%H%M%S).dump
Verifica el dump pasándolo por el pg_restore del contenedor:
docker compose exec -T postgres pg_restore --list < /opt/backups/docker/mydb-20260319-120000.dump | head -10
Esto imprime la tabla de contenidos sin restaurar nada. Si el archivo está corrupto, pg_restore mostrará un error. Usamos docker compose exec -T porque pg_restore vive dentro del contenedor, no en el host (a menos que instales postgresql-client por separado).
¿Cómo hago backup de una base de datos MySQL en Docker?
Usa mysqldump con --single-transaction para tablas InnoDB. Esto da un snapshot consistente sin bloquear la base de datos.
docker compose exec -T mysql mysqldump \
-u root \
-p"$MYSQL_ROOT_PASSWORD" \
--single-transaction \
--routines \
--triggers \
mydb > /opt/backups/docker/mydb-$(date +%Y%m%d-%H%M%S).sql
El flag -p no tiene espacio antes de la contraseña. --single-transaction usa una lectura consistente para tablas InnoDB. --routines y --triggers incluyen procedimientos almacenados y triggers que mysqldump omite por defecto.
Verifica que el dump no está vacío y termina con el marcador de finalización:
tail -5 /opt/backups/docker/mydb-20260319-120000.sql
La salida muestra -- Dump completed on YYYY-MM-DD HH:MM:SS. Si el archivo está truncado o vacío, el dump falló.
¿Cómo automatizo backups de volúmenes Docker con offen/docker-volume-backup?
offen/docker-volume-backup se ejecuta como contenedor sidecar en tu stack Compose. Hace backup de los volúmenes montados según un horario, cifra opcionalmente los archivos con GPG, y puede subir directamente a almacenamiento compatible con S3. También puede detener contenedores durante el backup para asegurar consistencia.
Añade el servicio de backup a tu docker-compose.yml:
services:
# ... your existing services ...
backup:
image: offen/docker-volume-backup:v2.47.2
restart: unless-stopped
env_file:
- ./backup.env
volumes:
- myapp_data:/backup/myapp_data:ro
- myapp_db:/backup/myapp_db:ro
- /opt/backups/docker:/archive
- /var/run/docker.sock:/var/run/docker.sock
labels:
- docker-volume-backup.stop-during-backup=false
Qué hace esto: monta cada volumen a respaldar bajo /backup/ como solo lectura, monta /archive para almacenamiento local de backups, y monta el socket Docker para que la herramienta pueda detener y reiniciar contenedores cuando está configurado para ello. El tag de imagen v2.47.2 fija la versión. No uses latest en producción.
Nota de seguridad: montar el socket Docker da al contenedor de backup control total sobre Docker en el host. Esto es necesario para la función de detención durante el backup. Si no necesitas esa función, puedes montarlo como solo lectura (/var/run/docker.sock:/var/run/docker.sock:ro), lo que permite a la herramienta leer las etiquetas de los contenedores pero le impide detenerlos o iniciarlos.
Crea el archivo de entorno con permisos restringidos:
touch /opt/myapp/backup.env
chmod 600 /opt/myapp/backup.env
# /opt/myapp/backup.env
BACKUP_CRON_EXPRESSION=0 3 * * *
BACKUP_RETENTION_DAYS=7
BACKUP_COMPRESSION=gz
BACKUP_FILENAME=backup-%Y%m%dT%H%M%S.tar.gz
# GPG encryption (generate a strong passphrase)
GPG_PASSPHRASE=your-generated-passphrase-here
# S3-compatible storage (optional, see rclone section for alternative)
# AWS_S3_BUCKET_NAME=my-backups
# AWS_S3_PATH=myapp
# AWS_ENDPOINT=s3.eu-central-1.amazonaws.com
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
Genera la passphrase GPG:
openssl rand -base64 32
Guarda esta passphrase en un lugar seguro fuera del servidor. Si la pierdes, los backups cifrados son irrecuperables.
Si quieres que la herramienta de backup detenga ciertos contenedores durante el backup para asegurar la consistencia del sistema de archivos, añade una etiqueta a esos servicios:
services:
app:
# ... your config ...
labels:
- docker-volume-backup.stop-during-backup=true
Inicia el servicio de backup:
docker compose up -d backup
Verifica que está ejecutándose:
docker compose logs backup
La salida muestra una línea de log confirmando el horario cron. Espera la primera ejecución programada, o dispara un backup manual:
docker compose exec backup backup
Comprueba que el archivo apareció:
ls -lh /opt/backups/docker/
¿Cómo programo backups de Docker con cron?
Para las estrategias basadas en tar y dump de bases de datos, un script shell con cron gestiona la programación y la retención. La herramienta offen tiene su propio scheduler; sáltate esta sección si solo usas esa.
Crea el script de backup:
touch /opt/backups/docker-backup.sh
chmod 700 /opt/backups/docker-backup.sh
#!/usr/bin/env bash
# /opt/backups/docker-backup.sh
# Backs up Docker volumes and databases, removes old archives.
set -euo pipefail
BACKUP_DIR="/opt/backups/docker"
RETENTION_DAYS=7
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
COMPOSE_DIR="/opt/myapp"
# Load database credentials from env file
source "${COMPOSE_DIR}/.env"
cd "$COMPOSE_DIR"
# --- Tar backup of application data volume ---
docker compose stop app
docker run --rm \
-v myapp_data:/source:ro \
-v "${BACKUP_DIR}":/backup \
alpine tar czf "/backup/myapp_data-${TIMESTAMP}.tar.gz" -C /source .
docker compose start app
# --- PostgreSQL dump (no downtime) ---
docker compose exec -T postgres pg_dump \
-U "$POSTGRES_USER" \
-Fc --no-owner --no-acl \
"$POSTGRES_DB" > "${BACKUP_DIR}/${POSTGRES_DB}-${TIMESTAMP}.dump"
# --- Retention: delete backups older than N days ---
find "$BACKUP_DIR" -type f -name "*.tar.gz" -mtime +${RETENTION_DAYS} -delete
find "$BACKUP_DIR" -type f -name "*.dump" -mtime +${RETENTION_DAYS} -delete
echo "[$(date -Iseconds)] Backup completed successfully"
Verifica que el script funciona sin errores:
/opt/backups/docker-backup.sh
Comprueba los archivos generados:
ls -lh /opt/backups/docker/
Añade una entrada cron que se ejecute a las 03:00 diariamente y registre la salida:
crontab -e
0 3 * * * /opt/backups/docker-backup.sh >> /var/log/docker-backup.log 2>&1
El 2>&1 redirige stderr al mismo archivo de log, para que los errores queden capturados. Revisa el log después de la primera ejecución:
cat /var/log/docker-backup.log
Si el script falla, cron se traga el error silenciosamente a menos que redirijas la salida. Para recibir alertas por correo en caso de fallo, añade este wrapper:
0 3 * * * /opt/backups/docker-backup.sh >> /var/log/docker-backup.log 2>&1 || echo "Docker backup failed on $(hostname)" | mail -s "BACKUP FAILED" you@example.com
Esto requiere mailutils o un paquete similar. Ajusta la dirección del destinatario.
¿Cómo copio backups de Docker a almacenamiento compatible con S3 usando rclone?
Los backups locales protegen contra fallos de aplicación. No protegen contra fallos de disco o un servidor comprometido. Necesitas copias fuera del servidor. rclone funciona con cualquier almacenamiento compatible con S3: AWS S3, Backblaze B2, Wasabi, MinIO, OVH Object Storage, Scaleway, y otros.
Instala rclone:
apt update && apt install -y rclone
Configura un remote compatible con S3:
rclone config
Sigue las indicaciones interactivas:
npara nuevo remote- Nómbralo
s3backup - Elige
s3(Amazon S3 Compliant Storage Providers) - Selecciona tu proveedor (o «Any other S3 compatible provider»)
- Introduce tu clave de acceso y clave secreta
- Configura la región y la URL del endpoint de tu proveedor
- Deja las demás opciones en sus valores por defecto
Verifica que el remote funciona:
rclone lsd s3backup:
Esto lista tus buckets. Si falla, tus credenciales o endpoint son incorrectos.
Crea un bucket para backups (si tu proveedor lo soporta vía rclone):
rclone mkdir s3backup:my-docker-backups
Sincroniza tu directorio local de backups con el bucket:
rclone sync /opt/backups/docker s3backup:my-docker-backups/$(hostname)/ \
--transfers 4 \
--checkers 8 \
--log-file /var/log/rclone-backup.log \
--log-level INFO
Qué hace esto: sync hace que el remote coincida con el directorio local. Los archivos eliminados localmente (por la retención) también se eliminan en el remote. El prefijo $(hostname) separa los backups si tienes varios servidores.
Verifica la subida:
rclone ls s3backup:my-docker-backups/$(hostname)/
La salida muestra tus archivos de backup listados con tamaños que coinciden con las copias locales.
Añade rclone sync al script de backup o como entrada cron separada que se ejecute después del backup:
30 3 * * * rclone sync /opt/backups/docker s3backup:my-docker-backups/$(hostname)/ --transfers 4 --log-file /var/log/rclone-backup.log --log-level INFO
Esto se ejecuta a las 03:30, dando al trabajo de backup de las 03:00 tiempo para terminar.
Protege la configuración de rclone: contiene tus credenciales de S3.
chmod 600 ~/.config/rclone/rclone.conf
ls -la ~/.config/rclone/rclone.conf
La salida muestra permisos -rw-------. Solo root puede leer este archivo.
¿Debo detener los contenedores antes de hacer backup de volúmenes Docker?
Depende de lo que contenga el volumen. Equivocarse aquí es la causa más común de backups corruptos.
Bases de datos (PostgreSQL, MySQL, MongoDB): nunca hagas tar de un volumen de base de datos en ejecución. Los archivos en disco representan un estado de transacción en curso. Un tar de esos archivos es como fotocopiar un libro mientras alguien reescribe capítulos. El resultado es internamente inconsistente. Usa pg_dump, mysqldump o mongodump en su lugar. Estas herramientas producen un snapshot transaccionalmente consistente mientras la base de datos sigue funcionando.
Datos de aplicación (uploads, archivos estáticos, config): tar es seguro si la aplicación tolera una parada breve. Si la aplicación escribe continuamente y no puedes detenerla, el tar puede contener archivos escritos parcialmente. Para la mayoría de aplicaciones web, una parada de 2 segundos durante un backup a las 3 de la madrugada es aceptable.
Redis, almacenes clave-valor: Redis escribe snapshots RDB a disco periódicamente. Dispara un BGSAVE antes de hacer tar del volumen, luego espera a que termine. Esto te da un snapshot consistente sin detener Redis.
docker compose exec redis redis-cli BGSAVE
# Wait a few seconds
docker compose exec redis redis-cli LASTSAVE
La opción segura por defecto: en caso de duda, detén el contenedor, haz backup, reinicia. Un breve tiempo de inactividad es mejor que backups corruptos.
¿Cómo restauro volúmenes Docker en un nuevo VPS?
Este es el procedimiento que demuestra que tus backups funcionan. Instala Docker en un servidor nuevo, transfiere los archivos de backup, recrea los volúmenes, restaura los datos, y verifica que la aplicación funciona.
1. Instalar Docker en el nuevo VPS
apt update && apt install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update && apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Verifica:
docker --version && docker compose version
2. Transferir los archivos de backup al nuevo servidor
Desde tu máquina local o el servidor antiguo:
rsync -avz --progress /opt/backups/docker/ root@NEW_SERVER_IP:/opt/backups/docker/
O descarga desde S3:
# On the new server, install and configure rclone first
apt install -y rclone
# Re-run rclone config with the same credentials
rclone copy s3backup:my-docker-backups/OLD_HOSTNAME/ /opt/backups/docker/
Verifica que los archivos llegaron:
ls -lh /opt/backups/docker/
3. Copia tus archivos Compose y env
rsync -avz /opt/myapp/ root@NEW_SERVER_IP:/opt/myapp/
O restáuralos desde tu repositorio Git. Tu docker-compose.yml y .env deberían estar versionados. El archivo .env debería estar en .gitignore y respaldado por separado.
4. Restaurar el volumen basado en tar
# Create the volume (Docker Compose will also do this on first `up`,
# but creating it explicitly lets us restore data before starting services)
docker volume create myapp_data
# Restore from archive
docker run --rm \
-v myapp_data:/target \
-v /opt/backups/docker:/backup:ro \
alpine sh -c "cd /target && tar xzf /backup/myapp_data-20260319-030000.tar.gz"
Qué hace esto: crea el volumen con nombre, luego ejecuta un contenedor temporal que extrae el archivo dentro. El directorio de backup se monta como solo lectura para prevenir accidentes.
Verifica los datos restaurados:
docker run --rm -v myapp_data:/data:ro alpine ls -la /data/
La salida muestra los mismos archivos que estaban en el volumen original.
5. Restaurar la base de datos PostgreSQL
Inicia solo el contenedor de la base de datos:
cd /opt/myapp
docker compose up -d postgres
Espera a que esté listo:
docker compose logs -f postgres
# Wait until you see "database system is ready to accept connections"
Restaura el dump:
docker compose exec -T postgres pg_restore \
-U "$POSTGRES_USER" \
-d "$POSTGRES_DB" \
--clean \
--if-exists \
--no-owner \
--no-acl \
< /opt/backups/docker/mydb-20260319-030000.dump
Qué hace esto: --clean elimina los objetos existentes antes de recrearlos. --if-exists previene errores si los objetos aún no existen. Esto hace que la restauración sea idempotente.
Verifica los datos:
docker compose exec postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "\dt"
La salida muestra tus tablas listadas. Ejecuta un conteo rápido en una tabla conocida:
docker compose exec postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT count(*) FROM your_table;"
6. Iniciar todos los servicios y verificar
docker compose up -d
Comprueba que todos los contenedores están ejecutándose:
docker compose ps
Cada servicio debería mostrar Up o running. Si algún contenedor está reiniciándose en bucle, revisa sus logs:
docker compose logs --tail 50 service_name
Prueba la aplicación desde fuera del servidor. Desde tu máquina local:
curl -I http://NEW_SERVER_IP:PORT
Deberías obtener una respuesta HTTP válida. Si la aplicación tiene un endpoint de health check, consúltalo:
curl http://NEW_SERVER_IP:PORT/health
Para más información sobre health checks, consulta nuestra guía sobre límites de recursos y health checks de Docker Compose.
¿Cómo verifico que un backup de volumen Docker es válido?
Un backup que nunca has probado es un pasivo. Ejecuta estas comprobaciones regularmente, no solo durante la recuperación ante desastres.
Comprobar integridad del archivo:
# For tar.gz files
gzip -t /opt/backups/docker/myapp_data-20260319-030000.tar.gz && echo "OK" || echo "CORRUPT"
Comprobar contenido del archivo:
tar tzf /opt/backups/docker/myapp_data-20260319-030000.tar.gz | wc -l
Compara el número de archivos con un backup conocido como bueno. Una caída repentina en el número de archivos indica un problema.
Probar restauración en un volumen desechable:
docker volume create test_restore
docker run --rm \
-v test_restore:/target \
-v /opt/backups/docker:/backup:ro \
alpine sh -c "cd /target && tar xzf /backup/myapp_data-20260319-030000.tar.gz"
# Inspect the restored data
docker run --rm -v test_restore:/data:ro alpine ls -la /data/
# Clean up
docker volume rm test_restore
Verificar un dump de base de datos:
docker compose exec -T postgres pg_restore --list < /opt/backups/docker/mydb-20260319-030000.dump | wc -l
Si devuelve un número de objetos (tablas, índices, secuencias), el dump es legible. Si da error, el archivo está corrupto.
Generar checksums para almacenamiento a largo plazo:
sha256sum /opt/backups/docker/*.tar.gz /opt/backups/docker/*.dump > /opt/backups/docker/checksums-$(date +%Y%m%d).sha256
Sube el archivo de checksums junto con los backups. Antes de restaurar, verifica:
sha256sum -c /opt/backups/docker/checksums-20260319.sha256
Resolución de problemas
«Permission denied» al crear el archivo tar:
El contenedor temporal se ejecuta como root por defecto, así que esto normalmente significa que el directorio de backup no existe o tiene permisos incorrectos. Ejecuta ls -la /opt/backups/ y verifica que el subdirectorio docker existe con permisos 700.
pg_dump/pg_restore se queda colgado:
Probablemente olvidaste el flag -T en docker compose exec. Sin -T, exec intenta asignar un TTY, lo que bloquea al redirigir la salida. Usa docker compose exec -T.
Los archivos de backup tienen 0 bytes:
El contenedor escribió en una ruta diferente a la esperada. Verifica que el nombre del volumen en docker volume ls coincide con el que usaste en el flag -v. Los volúmenes con nombre distinguen mayúsculas de minúsculas.
rclone sync da timeout:
Las sincronizaciones iniciales grandes pueden exceder los timeouts por defecto. Añade --timeout 30m y --retries 3 al comando rclone.
offen/docker-volume-backup no se ejecuta según lo programado:
Comprueba la sintaxis de BACKUP_CRON_EXPRESSION. La herramienta usa sintaxis cron estándar de 5 campos. Ejecuta docker compose logs backup y busca errores de parseo.
La base de datos restaurada tiene permisos incorrectos:
Usaste un dump sin --no-owner. El dump intenta asignar la propiedad al usuario original, que puede no existir en el nuevo servidor. Rehaz el dump con --no-owner --no-acl o ejecuta REASSIGN OWNED BY old_user TO new_user; en psql.
Próximos pasos
- Configuración completa de Docker en producción
- Health checks que confirmen que los servicios están activos después de una restauración
- Fundamentos de la CLI de Docker
Copyright 2026 Virtua.Cloud. Todos los derechos reservados. Este contenido es una obra original del equipo de Virtua.Cloud. La reproducción, republicación o redistribución sin permiso escrito está prohibida.
¿Listo para probarlo?
Despliega Docker con copias de seguridad automatizadas en tu VPS.
Ver planes VPS