Instalar ERPNext en un VPS con Docker
Despliega ERPNext en tu VPS con Docker: 11 contenedores, TLS, correo saliente, versiones fijadas y una restauración de copia de seguridad probada.
Lo que implica ponerlo en producción
Alojar ERPNext en un VPS es una tarea de operaciones, no una instalación de un solo comando. La pila oficial de Docker Compose consta de once contenedores y contiene el libro mayor y los registros de clientes. Esto eleva el nivel exigido para 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 que la sustenta. 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 subidos. 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 mantenido por el proyecto. Todos los comandos siguientes se comprobaron con ese repositorio en agosto de 2026. Si Docker Compose es nuevo para usted, ejecutar Docker Compose en un VPS explica los conceptos en los que se basa esta guía.
¿Cuántos recursos necesita ERPNext en un VPS?
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 empieza con 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 documentos determina la cifra real. La última fila no es un mínimo publicado. Es, aproximadamente, el punto en el que la memoria deja de ser el recurso que debe vigilar.
Sea directo con los planes pequeños. Un VPS de 1 GB o 2 GB iniciará la pila, pero fallará con la primera importación o 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 mediante el asesino de procesos por falta de memoria, y docker inspect muestra después "OOMKilled": true con el código de salida 137. Si se mata un worker durante un trabajo, el documento enviado queda con su tarea en segundo plano incompleta.
Para una empresa que usa ERPNext todos los días, 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 guarda 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í se encuentrabench.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 tareas en segundo plano, como el correo saliente, las importaciones y la generación de informes.schedulerejecuta las tareas programadas por tiempo, incluidos los informes planificados y los documentos de repetición automática.websocketes el proceso de socket.io que proporciona actualizaciones en tiempo real en el navegador.dbes MariaDB.redis-cacheyredis-queueson dos instancias independientes de Redis: una para la caché y otra para la cola de tareas.
Conviene conocer esta separación porque indica qué registro debe revisar. Un correo atascado es un problema del worker de la cola, por lo que docker compose logs -f queue-short es el comando adecuado. Si una página carga, pero la insignia de notificaciones nunca se actualiza, el problema está en el websocket. Revisar los registros de backend en cualquiera de los dos casos le hará perder una tarde.
Instale con los archivos de Compose de producción, no con los de demostración
El repositorio incluye pwd.yml, y el README lo indica claramente: "This setup is intended for short-lived evaluation only. You will not be able to install custom apps to this setup." Úselo para revisar ERPNext durante una tarde. No lo utilice para operar 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 incluye como 123 en el archivo de ejemplo. SITES_RULE es la regla de enrutamiento de Traefik, y LETSENCRYPT_EMAIL recibe las alertas 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 de Compose y, después, inícielo.
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 el archivo generado. Este paso adicional resulta útil: la pila en ejecución queda definida en un único archivo que puede leer y confirmar en el control de versiones, por lo que no puede cambiar sin que lo advierta si alguien edita el archivo env o si actualiza el repositorio. cómo se combinan varios archivos de 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, y 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 indica el usuario root. El ámbito % concede al usuario del sitio nuevo acceso desde cualquier host de esa red privada.
El segundo problema es el nombre del sitio. De forma predeterminada, el frontend determina qué sitio debe servir a partir de la cabecera HTTP Host, por lo que 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 de 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 hacia él 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 sin formato.
Deben cumplirse dos condiciones; de lo contrario, nunca se emitirá ningún certificado. El registro DNS A de erp.example.com ya debe apuntar al VPS. Los puertos 80 y 443 deben ser accesibles desde Internet, porque Let's Encrypt verifica que controla el nombre mediante un desafío HTTP-01 en el puerto 80. Compruebe también el firewall de red de su proveedor y el 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 Docker Compose muestra cómo compartir el proxy en lugar de competir por el puerto 443.
Correo saliente, o las facturas nunca salen del servidor
Este es el paso que la mayoría de las guías de ERPNext omiten 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 entrega y ningún informe programado se envía. 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, y lo que consigue salir se rechaza o se clasifica como spam porque una dirección de VPS nueva no tiene reputación de envío. Use un relay autenticado en el puerto 587.
La vía 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 la línea de comandos, para que se almacene cifrado y nunca entre en el historial de su shell.
Después, 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 lo que 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 y añada una política DMARC. Sin estos registros, una factura técnicamente correcta acaba en la carpeta de spam del cliente. Si prefiere controlar todo el recorrido, 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 restauran
Un volcado de 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 sitios, no en MariaDB. Si sólo restaura la base de datos, todas las órdenes de compra cargadas vuelven como enlaces rotos.
docker compose --project-name erpnext exec backend \
bench --site erp.example.com backup --with-filesEso escribe cuatro archivos en sites/erp.example.com/private/backups dentro del volumen sites:
- un volcado de
-database.sql.gz - un archivo de
-files.tarcon los archivos públicos - un archivo de
-private-files.tarcon los archivos privados - una copia de la configuración del sitio en
-site_config_backup.json
El cuarto archivo es el que se suele descartar, 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 integración. Si restaura una base de datos sin la clave correspondiente, el sitio 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 eso desde cron y, después, envíe el directorio a una ubicación que no administre. las copias de seguridad cifradas de restic en almacenamiento externo son la herramienta 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 almacenarse cifrada en reposo, en hardware distinto de este.
Pruebe la restauración antes de necesitarla
Una copia de seguridad sin probar es una suposición. Restáurela en un segundo sitio del mismo servidor, nunca en el sitio activo.
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 cuentas por cobrar y compare el saldo de cierre con el del sitio activo. Abra una factura de compra reciente y descargue su archivo adjunto. Que un sitio muestre la 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 bloqueo 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 de esquema. bench migrate reescribe las tablas de la base de datos y puede reescribir los datos de los documentos, sin posibilidad de deshacer los cambios. Revertir la actualización requiere restaurar una copia de seguridad, no ejecutar un docker compose down.
Por tanto, 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 mantenga ese número 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 iniciar el proceso.
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 cree 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. Si omite versiones principales, ejecutará 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.
Protección 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 puede mantenerse en los entornos de 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 sin cifrar, por lo que chmod 600 el archivo y no lo incluya en ningún repositorio de git. Para usar una opción más segura, overrides/compose.mariadb-secrets.yaml lee la contraseña desde un archivo de Docker secret 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 sustitución de HTTPS, los únicos puertos expuestos son 80 y 443. No añada un mapeo 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 todas las cuentas que tengan el rol System Manager. Este 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 comodidad. Si ejecuta varias aplicaciones autohospedadas, Authentik como proveedor de inicio de sesión único autohospedado es mejor que usar otra contraseña para cada aplicación.
Aplique los parches del host y reinicie para instalar las actualizaciones del kernel. Antes de confiar en que la pila volverá a iniciarse, compruebe en el archivo generado que cada servicio tenga una política restart, porque una pila sin ella permanecerá 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 con comodidad en un solo VPS
Un VPS puede alojar una empresa pequeña durante mucho tiempo. Estas señales indican que ya no es suficiente:
- 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 tardan treinta, y MariaDB es el proceso que más CPU consume.
- Las copias de seguridad tardan tanto que una ejecución se solapa con la siguiente ejecución programada.
Empiece asignando a MariaDB recursos que no comparta con otros servicios, 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 memoria. Un servidor de aplicaciones más grande ayuda menos de lo que suele esperarse. 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 más 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 corrige el problema del que realmente se quejan los usuarios.
FAQ
¿Cuánta RAM necesita ERPNext en un VPS?
La documentación publicada parte de 4 GB con 2 vCPU, y ese nivel es sólo para evaluación. Para una empresa que lo usa a diario, planifique 8 GB, 4 vCPU y 100 GB de SSD. Por debajo de esos valores, el asesino de procesos por falta de memoria del kernel detiene los contenedores bajo carga. docker inspect lo registra como "OOMKilled": true con el código de salida 137. Estos valores son puntos de partida, no mediciones. Supervise su propio consumo de memoria durante el primer mes.
¿Puedo ejecutar pwd.yml en producción?
No. El README del proyecto indica que está destinado sólo a evaluaciones de corta duración y señala que no se pueden instalar aplicaciones personalizadas en él. Use compose.yaml con las anulaciones de MariaDB, Redis y HTTPS, genérelas en un solo 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 tanto, el nombre del sitio debe coincidir con el dominio del 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 de entorno con el nombre del sitio, vuelva a generar el archivo de compose y reinicie el stack.
¿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. bench --site erp.example.com backup --with-files genera los cuatro archivos. La copia de configuración contiene encryption_key. Por eso, una restauración sin ella no puede descifrar las contraseñas de integraciones almacenadas, lo que se manifiesta como Encryption key is invalid! Please check site_config.json.
¿Cómo actualizo ERPNext sin dañar mis datos?
Haga una copia de seguridad con --with-files, active el modo de mantenimiento, cambie ERPNEXT_VERSION en el archivo de entorno, vuelva a generar el archivo de compose, descargue las imágenes, inicie el stack, ejecute bench --site erp.example.com migrate y desactive el modo de mantenimiento. Avance una versión principal 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 deshacer los cambios. Para volver atrás, restaure la copia de seguridad que creó al principio.