Cómo alojar ERPNext en un VPS con Docker
Despliega ERPNext en un VPS con Docker: pila de 11 contenedores, TLS, correo saliente, versiones fijadas y restauración de copias probada.
Lo que implica esta instalación
Alojar ERPNext en un VPS es una tarea de operación, no una instalación de un solo comando. La pila oficial de Docker Compose tiene once contenedores y almacena el libro mayor y los registros de clientes. Esto eleva el nivel de exigencia de todo lo que sigue: una copia de seguridad no es una copia de seguridad hasta que se ha restaurado, y una etiqueta de imagen sin fijar es una migración de esquema a punto de producirse.
A lo largo del texto aparecen varios nombres. ERPNext es la aplicación empresarial. Frappe es el framework de Python en el que se basa. Bench es la herramienta de línea de comandos que administra los sitios y ya está instalada dentro de los contenedores. Un site es un tenant: una base de datos de MariaDB y un directorio de archivos cargados. Casi todos los comandos de esta guía se ejecutan bench dentro del contenedor backend contra un sitio identificado por su nombre.
Esta guía utiliza el repositorio frappe_docker, que es el despliegue que mantiene el proyecto. Todos los comandos siguientes se comprobaron con ese repositorio en agosto de 2026. Si Docker Compose todavía es nuevo para usted, ejecutar Docker Compose en un VPS explica los conceptos que esta guía da por supuestos.
¿Cuántos recursos de VPS necesita ERPNext?
The data behind this chart
[
{
"label": "Evaluation",
"vcpu": 2,
"ram_gb": 4,
"disk_gb": 40
},
{
"label": "Small production",
"vcpu": 4,
"ram_gb": 8,
"disk_gb": 100
},
{
"label": "Room to grow",
"vcpu": 4,
"ram_gb": 16,
"disk_gb": 160
}
]La documentación publicada parte de 2 vCPU y 4 GB de RAM antes de que inicie sesión un solo usuario. Ese es el nivel de evaluación. Son puntos de partida, no mediciones de esta guía, y el volumen de sus documentos determina la cifra real. La última fila ni siquiera es un mínimo publicado. Es aproximadamente el punto en el que la memoria deja de ser una preocupación.
Sea realista con los planes pequeños. Un VPS de 1 GB o 2 GB iniciará la pila, pero fallará en la primera importación o en el primer informe largo, porque nueve contenedores de ejecución prolongada, el buffer pool de MariaDB y un worker de Python que genera un informe no caben en esa memoria. El fallo no es controlado. El kernel detiene un contenedor por falta de memoria, y docker inspect muestra después "OOMKilled": true con el código de salida 137. Si se termina un worker mientras procesa un trabajo, el documento enviado queda con su tarea en segundo plano incompleta.
Para una empresa que usa ERPNext a diario, 8 GB de RAM, 4 vCPU y 100 GB de SSD son el mínimo realista. La RAM se agota primero. El disco crece más rápido de lo esperado, porque cada archivo adjunto y cada copia de seguridad local se guardan en el mismo volumen que la base de datos.
Los once contenedores y la función de cada uno
Ejecute docker compose ps cuando la pila esté activa y haya nueve contenedores en ejecución. Otros dos, configurator y create-site, realizan su trabajo una vez y salen. Por eso el total es de once.
backendejecuta la aplicación Frappe con gunicorn. Aquí residebench.frontendes nginx. Sirve los recursos estáticos y reenvía todo lo demás al backend.queue-shortyqueue-longson workers de RQ (Redis Queue). Ejecutan trabajos en segundo plano, como el envío de correo, las importaciones y la generación de informes.schedulerejecuta los trabajos programados, incluidos los informes planificados y los documentos de repetición automática.websocketes el proceso socket.io que gestiona las actualizaciones en tiempo real del navegador.dbes MariaDB.redis-cacheyredis-queueson dos instancias independientes de Redis: una para la caché y otra para la cola de trabajos.
Conviene entender esta separación porque indica qué registro debe consultar. Un correo atascado es un problema del worker de la cola, por lo que docker compose logs -f queue-short es el comando adecuado. Una página que carga pero nunca actualiza el distintivo de notificaciones tiene un problema de websocket. Consultar los registros de backend para cualquiera de los dos problemas le hará perder toda la tarde.
Instale con los archivos Compose de producción, no con los de demostración
El repositorio incluye pwd.yml y el README lo indica claramente: "Esta configuración sólo está pensada para evaluaciones de corta duración. No podrá instalar aplicaciones personalizadas en ella". Úsela para probar ERPNext durante una tarde. No la use para ejecutar una empresa.
sudo apt update && sudo apt install -y git
curl -fsSL https://get.docker.com | bash
git clone https://github.com/frappe/frappe_docker
cd frappe_docker
mkdir -p ~/gitops
cp example.env ~/gitops/erpnext.envAbra ~/gitops/erpnext.env y cambie cuatro valores. ERPNEXT_VERSION fija la etiqueta de la imagen. DB_PASSWORD se distribuye como 123 en el archivo de ejemplo. SITES_RULE es la regla de enrutamiento de Traefik y LETSENCRYPT_EMAIL recibe las advertencias de los certificados.
ERPNEXT_VERSION=v16.32.1
DB_PASSWORD=<a long random password>
SITES_RULE=Host(`erp.example.com`)
LETSENCRYPT_EMAIL=ops@example.comAhora genere un único archivo Compose y arránquelo.
docker compose --project-name erpnext \
--env-file ~/gitops/erpnext.env \
-f compose.yaml \
-f overrides/compose.mariadb.yaml \
-f overrides/compose.redis.yaml \
-f overrides/compose.https.yaml \
config > ~/gitops/erpnext.yaml
docker compose --project-name erpnext -f ~/gitops/erpnext.yaml up -dconfig no inicia nada. Combina el archivo base con las anulaciones e imprime el resultado con todas las variables ya sustituidas. Después, ejecute ese archivo generado. El paso adicional resulta útil: la pila en ejecución queda definida en un archivo que puede leer y confirmar en el repositorio, por lo que no cambia sin que lo advierta si alguien edita el archivo env o si actualiza el repositorio. cómo se combinan varios archivos Docker Compose explica en detalle las reglas de anulación.
Espere a que db se inicie y a que configurator termine, lo que tarda unos segundos. Después, cree el sitio.
docker compose --project-name erpnext exec backend \
bench new-site --mariadb-user-host-login-scope=% \
--db-root-password '<your DB_PASSWORD>' \
--install-app erpnext \
--admin-password '<a strong admin password>' \
erp.example.comCompruébelo:
docker compose --project-name erpnext ps
docker compose --project-name erpnext exec backend bench --site erp.example.com list-appslist-apps debe mostrar frappe y erpnext con sus versiones. Un ps en buen estado muestra nueve servicios en estado running y ninguno en restarting.
Aquí suelen producirse dos problemas. --mariadb-user-host-login-scope=% no es opcional con Docker. El contenedor de la aplicación accede a MariaDB a través de la red de Docker, por lo que llega como un host remoto. Un usuario de base de datos limitado a localhost no puede iniciar sesión desde allí. La creación del sitio falla entonces con un error de acceso denegado de MariaDB que identifica al usuario root. El ámbito % permite que el usuario del sitio nuevo acceda desde cualquier host de esa red privada.
El segundo problema es el nombre del sitio. De forma predeterminada, el frontend elige el sitio que debe servir a partir de la cabecera HTTP Host. Por tanto, un sitio creado como erpnext no está disponible en erp.example.com aunque ambos existan. Asigne al sitio el nombre del dominio, como se ha indicado arriba, o establezca FRAPPE_SITE_NAME_HEADER en el archivo env con el nombre del sitio y vuelva a generar el archivo Compose.
HTTPS y qué debe cumplirse antes de que funcione
El override de compose.https.yaml ejecuta Traefik en el puerto 443, redirige el puerto 80 a ese puerto y solicita certificados a Let's Encrypt. TLS (seguridad de la capa de transporte) evita que una factura y una cookie de sesión circulen por la red en texto plano.
Deben cumplirse dos condiciones. De lo contrario, no se emitirá ningún certificado. El registro DNS A de erp.example.com ya debe apuntar a la VPS. Los puertos 80 y 443 deben ser accesibles desde Internet, porque Let's Encrypt demuestra que controla el nombre mediante un desafío HTTP-01 en el puerto 80. Compruebe también el firewall de red del proveedor y el firewall del servidor. Son controles independientes, y el firewall del panel es el que suele olvidarse.
Los certificados se guardan en el volumen cert-data, en /letsencrypt/acme.json. Si el navegador muestra un certificado predeterminado en lugar del suyo, busque el nombre del servicio proxy en docker compose --project-name erpnext ps y revise sus registros para localizar el error de ACME (entorno de gestión automática de certificados). ¿Ejecuta otras aplicaciones web en el mismo servidor? una instancia de Traefik delante de varias aplicaciones de Docker Compose muestra cómo compartir el proxy en lugar de intentar usar el puerto 443 desde varios servicios. La segunda aplicación de un servidor como este suele estar orientada a clientes, y una mesa de soporte Chatwoot autoalojada se ejecuta detrás del mismo proxy. Así, las personas que gestionan las facturas también pueden responder al correo y al chat de los clientes desde un solo lugar.
Correo saliente o las facturas nunca salen del servidor
Este es el paso que omiten la mayoría de las guías de ERPNext y el que determina si el sistema resulta útil. Sin correo saliente operativo, ninguna factura llega al cliente, ningún restablecimiento de contraseña se recibe y ningún informe programado se entrega. La pila no incluye un servidor de correo.
No intente enviar correo directamente desde el VPS por el puerto 25. La mayoría de los proveedores bloquean el puerto saliente 25 en las cuentas nuevas. Además, los mensajes que consiguen salir suelen rechazarse o clasificarse como spam porque la dirección de un VPS nuevo no tiene reputación de envío. Use un relay autenticado en el puerto 587.
La ruta compatible es la pantalla Email Account de la interfaz de ERPNext, que almacena la contraseña cifrada. También puede escribir las claves en la configuración del sitio:
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-config mail_server smtp.example.com
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-config mail_port 587 --parse
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-config use_tls 1 --parse
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-config mail_login 'erp@example.com'
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-config auto_email_id 'erp@example.com'--parse almacena 587 como un número en lugar de la cadena "587". Vuelva a leer el archivo y confirme que esos dos valores no tienen comillas:
docker compose --project-name erpnext exec backend \
cat sites/erp.example.com/site_config.jsonConfigure mail_password desde la pantalla Email Account en lugar de hacerlo en la línea de comandos. Así se almacena cifrado y nunca entra en el historial de la shell.
A continuación, envíe un mensaje real. Cree una Sales Invoice, envíela por correo a una dirección que controle y supervise la cola mientras lo hace:
docker compose --project-name erpnext logs -f queue-shortEl correo saliente es un trabajo en segundo plano. Por eso, un mensaje que nunca llega suele aparecer como un trabajo fallido en ese registro y no como un error en el navegador. Publique también registros SPF (sender policy framework) y DKIM (domainkeys identified mail) para el dominio de envío. Después, añada una política DMARC. Sin estos registros, una factura técnicamente correcta puede acabar en la carpeta de spam del cliente. Si prefiere controlar toda la ruta, un servidor de correo Mailcow autohospedado le proporciona un relay bajo su control, en un servidor separado del ERP.
Copias de seguridad que realmente se pueden restaurar
Un volcado de la base de datos, por sí solo, no es una copia de seguridad de ERPNext. Los archivos adjuntos y los archivos privados se encuentran en el directorio de sites, no en MariaDB. Si restaura únicamente la base de datos, todos los pedidos de compra cargados vuelven como enlaces rotos.
docker compose --project-name erpnext exec backend \
bench --site erp.example.com backup --with-filesEsto escribe cuatro archivos en sites/erp.example.com/private/backups dentro del volumen sites:
- un volcado de
-database.sql.gz - un archivo
-files.tarcon los archivos públicos - un archivo
-private-files.tarcon los archivos privados - una copia
-site_config_backup.jsonde la configuración del sitio
El cuarto archivo es el que muchos descartan, y es el que causa más problemas. Contiene encryption_key, la clave que Frappe usa para cifrar las contraseñas almacenadas: credenciales de cuentas de correo, claves de pasarelas de pago y todos los secretos de las integraciones. Si restaura una base de datos sin la clave correspondiente, el sitio se carga normalmente, pero el envío de correo falla con:
frappe.exceptions.ValidationError: Encryption key is invalid! Please check site_config.jsonConserve siempre los cuatro archivos juntos.
Después, sáquelos del servidor. Una copia de seguridad dentro del volumen no sobrevive a la pérdida del servidor, y bench también la elimina: de forma predeterminada, borra de ese directorio las copias de seguridad con más de 24 horas.
docker compose --project-name erpnext cp \
backend:/home/frappe/frappe-bench/sites/erp.example.com/private/backups \
~/erpnext-backupsEjecute esto desde cron y, después, copie el directorio a una ubicación que no administre. Las copias de seguridad cifradas de restic en almacenamiento externo son la opción adecuada, porque cifran los datos antes de cargarlos y restic check demuestra que el repositorio todavía se puede leer. Una copia de seguridad de ERP es una copia de todo el libro contable, por lo que debe mantenerse cifrada en reposo y en hardware distinto de este servidor.
Pruebe la restauración antes de necesitarla
Una copia de seguridad que no se ha probado es sólo una suposición. Pruébela en un segundo sitio del mismo servidor, nunca en el sitio en producción.
docker compose --project-name erpnext exec backend \
bench new-site --mariadb-user-host-login-scope=% \
--db-root-password '<your DB_PASSWORD>' \
--admin-password '<a strong admin password>' \
restore-test.example.com
docker compose --project-name erpnext exec backend \
bench --site restore-test.example.com --force restore \
sites/erp.example.com/private/backups/<stamp>-erp.example.com-database.sql.gz \
--with-public-files sites/erp.example.com/private/backups/<stamp>-erp.example.com-files.tar \
--with-private-files sites/erp.example.com/private/backups/<stamp>-erp.example.com-private-files.tar \
--db-root-password '<your DB_PASSWORD>'Copie la clave de cifrado de la configuración respaldada al sitio restaurado. De lo contrario, sus integraciones seguirán sin funcionar:
docker compose --project-name erpnext exec backend \
bench --site restore-test.example.com set-config encryption_key '<value from site_config_backup.json>'Compruebe ahora la restauración como lo haría un contable. Abra el informe de Accounts Receivable y compare el saldo de cierre con el del sitio en producción. Abra una factura de compra reciente y descargue su archivo adjunto. Que un sitio muestre su página de inicio de sesión no demuestra nada.
Elimine el sitio de prueba cuando termine:
docker compose --project-name erpnext exec backend \
bench drop-site restore-test.example.comPor qué el fijado de versiones es más importante en ERPNext
En un sitio estático, una etiqueta de imagen sin fijar significa un reinicio inesperado. En ERPNext significa una migración del esquema. bench migrate reescribe tablas de la base de datos y puede reescribir datos de documentos, y no tiene deshacer. Volver atrás requiere restaurar una copia de seguridad, no un docker compose down.
Por eso, fije la etiqueta. ERPNEXT_VERSION=v16.32.1 era la versión fijada en el propio pwd.yml del repositorio en agosto de 2026. No use ese número en el futuro sin comprobarlo. Las versiones actuales aparecen en la página de versiones de frappe/erpnext, y las etiquetas de imagen disponibles están en Docker Hub. Lea las notas de la versión a la que va a actualizar antes de hacerlo.
La actualización comienza con una copia de seguridad y el modo de mantenimiento.
docker compose --project-name erpnext exec backend \
bench --site erp.example.com backup --with-files
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-maintenance-mode onEdite ERPNEXT_VERSION en ~/gitops/erpnext.env y, después, genere la configuración, descargue la imagen y ejecute la migración.
docker compose --project-name erpnext \
--env-file ~/gitops/erpnext.env \
-f compose.yaml \
-f overrides/compose.mariadb.yaml \
-f overrides/compose.redis.yaml \
-f overrides/compose.https.yaml \
config > ~/gitops/erpnext.yaml
docker compose --project-name erpnext -f ~/gitops/erpnext.yaml pull
docker compose --project-name erpnext -f ~/gitops/erpnext.yaml up -d
docker compose --project-name erpnext exec backend \
bench --site erp.example.com migrate
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-maintenance-mode offEl modo de mantenimiento es importante porque migrate modifica el esquema mientras se ejecuta. Si un usuario envía un documento contra una tabla parcialmente migrada, tendrá que reparar los registros manualmente.
Actualice una versión principal cada vez y haga una copia de seguridad entre cada paso. El código de migración de una versión está diseñado para actualizar desde la versión anterior. Por tanto, omitir versiones principales ejecuta migraciones en una combinación que nadie ha probado.
El repositorio también incluye overrides/compose.migrator.yaml, que añade un contenedor que ejecuta bench --site all migrate en cada inicio. Es práctico. Pero también significa que un docker compose up con una etiqueta modificada migra la base de datos de producción sin supervisión. En un sistema empresarial, ejecute migrate como una decisión explícita tomada ese mismo día.
Endurecimiento de un servidor que almacena registros de clientes
Cambie la contraseña de Administrator en el primer inicio de sesión. El archivo Compose de evaluación incluye admin como esa contraseña, y este hábito se traslada a producción.
Cambie DB_PASSWORD para que no use el valor 123 de example.env. Ese valor termina en ~/gitops/erpnext.yaml generado en texto plano, así que chmod 600 el archivo y manténgalo fuera de cualquier repositorio de git. Para obtener más seguridad, overrides/compose.mariadb-secrets.yaml lee la contraseña de un archivo de secreto de Docker en lugar de una variable de entorno. gestión de archivos de entorno y secretos en Docker Compose explica las ventajas y desventajas.
Publique sólo lo necesario. Con la configuración override de HTTPS, los únicos puertos expuestos son 80 y 443. No añada una asignación ports al servicio db para facilitar la conexión de un cliente de base de datos: eso expone MariaDB a Internet. Use docker compose --project-name erpnext exec backend bench mariadb en su lugar. En el host, permita 22, 80 y 443, deniegue el resto y compruebe también el firewall de red independiente del proveedor.
Active la autenticación de dos factores en System Settings para cada cuenta que tenga el rol System Manager. Ese rol puede leer todos los documentos y exportar todas las tablas, por lo que debe tratarlo como una cuenta de administrador y no como una simple comodidad. Si ejecuta varias aplicaciones autoalojadas, Authentik como proveedor de inicio de sesión único autoalojado es mejor que añadir otra contraseña para cada aplicación.
Aplique los parches del host y reinicie el sistema para instalar las actualizaciones del kernel. Antes de confiar en que la pila volverá a iniciarse, compruebe que el archivo generado tenga una política restart para cada servicio, porque una pila sin ella permanece detenida después de ese reinicio. hacer que una pila de Docker Compose vuelva a iniciarse después de un reinicio explica la configuración de systemd.
Cuando ERPNext deja de funcionar cómodamente en un VPS
Un VPS puede alojar una empresa pequeña durante mucho tiempo. Estas son señales de que ya no basta:
- Los trabajos en segundo plano se acumulan, por lo que los correos y las importaciones llegan con minutos u horas de retraso.
docker inspectmuestra contenedores con"OOMKilled": trueo con el código de salida 137.- Los informes que tardaban dos segundos ahora tardan treinta, y MariaDB es el proceso que más CPU consume.
- Las copias de seguridad tardan tanto que una se solapa con la siguiente ejecución programada.
Empiece asignando a MariaDB recursos que no comparta, porque la base de datos y los workers de Python compiten por la misma memoria, y el buffer pool es el componente que necesita más. Un servidor de aplicaciones más grande ayuda menos de lo esperado. ejecutar la base de datos en Docker o en el host explica esta decisión, y configurar límites de memoria en Docker Compose evita que un contenedor deje sin recursos a los demás mientras realiza el cambio.
Después, añada workers de cola en lugar de aumentar la capacidad web. El trabajo lento de ERPNext se ejecuta en segundo plano: la generación de informes y las importaciones masivas. Añadir más contenedores de workers cuesta menos que ampliar el servidor y resuelve el problema que los usuarios realmente perciben.
FAQ
¿Cuánta RAM necesita ERPNext en un VPS?
La documentación publicada recomienda empezar con 4 GB y 2 vCPU, pero ese nivel sólo sirve para evaluación. Para una empresa que lo usa a diario, planifique 8 GB y 4 vCPU con 100 GB de SSD. Por debajo de ese nivel, el asesino de procesos por falta de memoria del kernel detiene los contenedores bajo carga, lo que docker inspect informa como "OOMKilled": true con el código de salida 137. Son valores iniciales, no mediciones, así que supervise el uso real de memoria durante el primer mes.
¿Puedo ejecutar pwd.yml en producción?
No. El README del proyecto indica que está pensado únicamente para evaluaciones de corta duración y señala que no permite instalar aplicaciones personalizadas. Use compose.yaml con las sobrescrituras de MariaDB, Redis y HTTPS, genérelas en un único archivo con docker compose config y ejecute ese archivo.
¿Por qué no se puede acceder a mi sitio de ERPNext justo después de crearlo?
De forma predeterminada, el frontend elige qué sitio servir a partir de la cabecera HTTP Host, por lo que el nombre del sitio debe coincidir con el dominio usado en el navegador. Un sitio creado como erpnext no se sirve en erp.example.com. Cree el sitio usando el dominio como nombre o establezca FRAPPE_SITE_NAME_HEADER en el archivo env con el nombre del sitio, genere de nuevo el archivo compose y reinicie la pila.
¿Qué debe incluir una copia de seguridad de ERPNext?
Cuatro archivos, conservados juntos: el volcado de -database.sql.gz, los archivos -files.tar y -private-files.tar, y la copia de configuración -site_config_backup.json. La ejecución de bench --site erp.example.com backup --with-files genera los cuatro. La copia de configuración contiene encryption_key, por lo que una restauración sin ella deja sin posibilidad de descifrado las contraseñas de integración almacenadas; esto se manifiesta como Encryption key is invalid! Please check site_config.json.
¿Cómo actualizo ERPNext sin dañar los datos?
Haga una copia de seguridad con --with-files, active el modo de mantenimiento, cambie ERPNEXT_VERSION en el archivo env, genere de nuevo el archivo compose, descargue las imágenes, inicie la pila, ejecute bench --site erp.example.com migrate y desactive el modo de mantenimiento. Avance una versión mayor cada vez y lea primero las notas de la versión, porque migrate reescribe el esquema y los datos de los documentos sin posibilidad de deshacerlo. Volver atrás implica restaurar la copia de seguridad creada al principio.