ERP y CRM propio: Odoo en Docker con HTTPS

Base de datos, servidor Odoo y proxy inverso con certificado propio

Guía técnica para levantar Odoo (ERP, CRM, facturación e inventario en una sola plataforma) con Docker Compose, con PostgreSQL como base de datos, HTTPS propio mediante un proxy inverso y las dos medidas de hardening que cualquier instalación real debería tener desde el primer día: contraseña maestra propia y gestor de bases de datos cerrado al público.

1. Introducción

Odoo es una suite de gestión empresarial de código abierto: CRM, facturación, inventario, ventas, proyectos y decenas de módulos más, todos integrados sobre la misma base de datos. Esta guía lo levanta con Docker Compose, con PostgreSQL como base de datos y un proxy inverso con HTTPS propio delante, siguiendo el mismo patrón ya probado en otras prácticas de este canal.

Odoo no obliga a usar HTTPS para las pantallas básicas de gestión, pero varias funciones reales sí lo necesitan: las notificaciones en tiempo real del chat interno, el modo sin conexión del punto de venta, o el escaneo de códigos de barras con la cámara, todas dependen de APIs del navegador que solo funcionan en un contexto seguro (HTTPS, o localhost). Por eso esta guía monta HTTPS desde el principio, con un proxy inverso y un certificado autofirmado, en vez de dejarlo para más adelante.

2. Variables de esta guía

Todos los bloques de configuración usan los siguientes marcadores en MAYÚSCULAS. Sustitúyelos por los valores reales del entorno donde se aplique esta guía.

MarcadorQué esDónde se obtiene
IP_HOST_DOCKERIP del host donde corre el stack con DockerLa interfaz de red de ese host
POSTGRES_PASSWORDContraseña del usuario odoo en la base de datosSe define en /docker/odoo/.env
MASTER_PASSWORD_ODOOContraseña maestra de Odoo: protege la creación y el borrado de bases de datos completasLa eliges tú; se escribe en odoo.conf (sección 6)
Si en el mismo servidor ya tienes otros servicios publicando puertos (por ejemplo otras prácticas de este canal en 8443 u 8444), comprueba que el puerto 8445 que usa esta guía está libre, o cámbialo por otro en las secciones 7, 8 y 11.

3. Arquitectura y flujo

Tres piezas: la base de datos, el propio Odoo, y un proxy inverso que sirve HTTPS delante de los dos.

Resumen visual
[Navegador, en cualquier equipo de la red]
      |
      |  https://IP_HOST_DOCKER:8445  (certificado autofirmado)
      v
[proxy] :8445  (Nginx, proxy inverso HTTPS)
      |
      v
[odoo] :8069  (ERP, sin exponer directamente al host)
      |
      v
[db]  (PostgreSQL)
Odoo no gestiona una única base de datos fija, como sí hacían Synapse o Vaultwarden: puede alojar varias bases de datos independientes en el mismo servidor (por ejemplo, una por cliente o una de pruebas y otra real), cada una con sus propios usuarios y datos. La contraseña maestra (sección 6) es justo lo que protege esa capacidad de crear o borrar bases de datos enteras, no el acceso a una empresa concreta.

4. Requisitos

  • Un host Linux con Docker Engine y el plugin Docker Compose (docker compose, sin guion) ya instalados.
  • No hace falta salida a Internet desde ese host para el funcionamiento básico: todo el flujo vive dentro de la red local.
  • Un navegador cualquiera en algún equipo de la misma red.
  • Al menos 2 GB de RAM libres para el contenedor de Odoo; con menos, el arranque puede ser muy lento o fallar bajo carga.

5. Levantar PostgreSQL y Odoo con Docker

Misma organización de siempre: cada stack en su carpeta dentro de /docker, credenciales en un .env aparte.

Crear la carpeta del stack
sudo mkdir -p /docker/odoo && cd /docker/odoo
sudo mkdir -p addons
Crear .env
sudo nano .env
.env
POSTGRES_PASSWORD=odoo_pwd

Y ahora el docker-compose.yml:

Crear docker-compose.yml
sudo nano docker-compose.yml
docker-compose.yml
services:
  db:
    image: postgres:16-alpine
    container_name: odoo-db
    environment:
      POSTGRES_USER: odoo
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: postgres
    volumes:
      - odoo-db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U odoo -d postgres"]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 20s
    restart: unless-stopped

  odoo:
    image: odoo:19.0
    container_name: odoo
    environment:
      HOST: db
      USER: odoo
      PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - odoo-web-data:/var/lib/odoo
      - ./addons:/mnt/extra-addons
      - ./odoo.conf:/etc/odoo/odoo.conf:ro
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  proxy:
    image: nginx:alpine
    container_name: proxy
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./proxy-selfsigned.crt:/etc/nginx/certs/proxy-selfsigned.crt:ro
      - ./proxy-selfsigned.key:/etc/nginx/certs/proxy-selfsigned.key:ro
    ports:
      - "8445:8445"
    depends_on:
      - odoo
    restart: unless-stopped

volumes:
  odoo-db-data:
  odoo-web-data:
El dato de la base de datos (odoo-db-data) y el de Odoo (odoo-web-data) van en volúmenes con nombre, no en carpetas del host: el proceso de Odoo dentro del contenedor corre con un usuario concreto (UID 101), y una carpeta del host montada directamente tendría el propietario equivocado, provocando errores de permisos al escribir datos. Con volúmenes con nombre, Docker gestiona esto sin que haya que tocar nada.
La carpeta ./addons sí es una carpeta normal del host: ahí es donde se colocarían módulos personalizados más adelante. Al ser de solo lectura para Odoo, no tiene el mismo problema de permisos.

6. Configurar Odoo (odoo.conf)

Crear odoo.conf
sudo nano odoo.conf
odoo.conf
[options]
addons_path = /mnt/extra-addons
admin_passwd = MASTER_PASSWORD_ODOO
db_host = db
db_port = 5432
db_user = odoo
db_password = odoo_pwd
proxy_mode = True
db_password aquí debe ser literal, igual que POSTGRES_PASSWORD del .env del paso 5: este archivo lo lee Odoo directamente dentro del contenedor, no pasa por la sustitución de variables de Docker Compose, así que no vale poner ${POSTGRES_PASSWORD} aquí.
Esta es la trampa de seguridad más repetida en instalaciones de Odoo expuestas a Internet: si no se fija admin_passwd a un valor propio, la instalación queda con la contraseña maestra por defecto, ampliamente conocida. Cualquiera que la sepa y llegue al gestor de bases de datos puede crear o borrar bases de datos completas. Pon aquí una contraseña larga y única, distinta de la de la base de datos. proxy_mode = True tampoco es opcional: le dice a Odoo que confíe en las cabeceras que le pasa el proxy inverso (IP real del visitante, esquema HTTPS), en vez de las del propio proxy.

7. Configurar el proxy inverso con HTTPS propio

Genera un certificado autofirmado con clave EC, válido para la IP del host:

Generar el certificado autofirmado
cd /docker/odoo
sudo openssl ecparam -name prime256v1 -genkey -noout -out proxy-selfsigned.key
sudo openssl req -new -x509 -key proxy-selfsigned.key -out proxy-selfsigned.crt -days 825 \
  -subj "/CN=IP_HOST_DOCKER" \
  -addext "subjectAltName=IP:IP_HOST_DOCKER"

Ahora la configuración de Nginx:

Crear nginx.conf
sudo nano nginx.conf
nginx.conf
resolver 127.0.0.11 valid=10s;

server {
    listen 8445 ssl;
    server_name IP_HOST_DOCKER;

    ssl_certificate     /etc/nginx/certs/proxy-selfsigned.crt;
    ssl_certificate_key /etc/nginx/certs/proxy-selfsigned.key;

    location / {
        set $upstream_odoo http://odoo:8069;
        proxy_pass $upstream_odoo;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
El resolver 127.0.0.11 (el DNS interno de Docker) junto con set $upstream_odoo; proxy_pass $upstream_odoo; es imprescindible: sin la variable, Nginx intenta resolver odoo una sola vez al arrancar, y si en ese instante el contenedor de Odoo (que tarda más en inicializar que Nginx) todavía no está listo, Nginx se niega a arrancar con un error de host not found in upstream.

8. Arrancar el stack y verificar

Arrancar los contenedores
cd /docker/odoo
docker compose up -d
docker compose ps

Sigue el log de Odoo, que tarda algo más que otros servicios en estar listo la primera vez (crea el esquema base de datos vacío):

Log de Odoo
docker logs -f odoo

Busca una línea similar a HTTP service (werkzeug) running, sin errores de conexión a la base de datos por el camino. Ctrl+C para salir del log.

9. Crear la primera base de datos

A diferencia de otras prácticas de este canal, Odoo no se configura por línea de comandos: la primera base de datos se crea desde un asistente web. Entra en:

URL de acceso
https://IP_HOST_DOCKER:8445
El navegador mostrará un aviso de conexión no privada o certificado no confiable. Es esperado: el certificado es autofirmado. Pulsa "Avanzado" y "Continuar a IP_HOST_DOCKER (no seguro)" (el texto exacto varía según el navegador) para seguir.

Rellena el formulario:

  • Master Password: el mismo valor que pusiste en admin_passwd (paso 6)
  • Nombre de la base de datos: el que quieras, por ejemplo clockwork
  • Email y contraseña: serán las credenciales del usuario administrador de esa empresa dentro de Odoo
  • Cargar datos de demostración: actívalo si es solo para probar y grabar; desactívalo para una instalación real de cliente
Al confirmar, Odoo tarda un minuto largo en crear el esquema completo de la base de datos. Al terminar, entra directamente en el escritorio de aplicaciones de Odoo con la sesión ya iniciada.

10. Cerrar el gestor de bases de datos

Con la base de datos ya creada, cierra el acceso público al gestor de bases de datos (la pantalla que permite crear, duplicar o borrar bases de datos enteras), para que solo sea accesible desde la propia configuración del servidor:

Editar odoo.conf
sudo nano /docker/odoo/odoo.conf
# Añade esta línea al final:
# list_db = False
Recrear el contenedor de Odoo
cd /docker/odoo
docker compose up -d --force-recreate odoo
Con list_db = False, la ruta del gestor de bases de datos deja de estar accesible desde fuera, aunque alguien conozca o adivine la contraseña maestra. Es el mismo tipo de cierre que ya hicisteis con el registro público de Vaultwarden: crear el recurso primero, cerrar el acceso público justo después.

11. Acceder desde el navegador

URL de acceso habitual
https://IP_HOST_DOCKER:8445
Con list_db = False y una sola base de datos creada, Odoo lleva directo a la pantalla de login de esa empresa, sin mostrar ningún selector. Inicia sesión con el email y la contraseña del paso 9, y confirma que el escritorio de aplicaciones carga con normalidad.

12. Tabla de puertos y componentes

PuertoComponenteSentido de la conexiónPara qué sirve
8445/tcpproxy (Docker, Nginx)Navegador → contenedorProxy inverso HTTPS (certificado autofirmado)
odoo (Docker, interno)proxy → odooServidor de aplicación de Odoo (Werkzeug); no publica ningún puerto al host
db (Docker, interno)odoo → dbPostgreSQL; no necesita exponerse al host

13. Errores comunes

ProblemaCausa probableSolución
El contenedor odoo no arranca, error de conexión a la base de datos db_password en odoo.conf no coincide con POSTGRES_PASSWORD del .env Revisar que ambos valores son literalmente iguales (sección 6)
El contenedor proxy se reinicia en bucle con host not found in upstream "odoo" Nginx intentó resolver el nombre del contenedor una sola vez al arrancar y Odoo todavía no estaba listo Confirmar que nginx.conf usa resolver 127.0.0.11 junto con la variable $upstream_odoo (sección 7), no un proxy_pass directo
El navegador o curl -k dan un error de protocolo TLS raro (no el aviso normal de certificado no confiable) Puede ser un problema del propio sistema, no del certificado Aislarlo con openssl s_server -key proxy-selfsigned.key -cert proxy-selfsigned.crt -accept 9999 -www fuera de Docker; si eso también falla, el problema es del sistema
El asistente de creación de base de datos no aparece, sale un error o una pantalla en blanco Odoo aún no ha terminado de arrancar del todo Esperar y revisar docker logs odoo hasta ver HTTP service (werkzeug) running
Tras poner list_db = False, ya no se puede crear una segunda base de datos Comportamiento esperado: esa es justo la protección que añade Para crear otra base de datos, poner temporalmente list_db = True, recrear el contenedor, crear la base de datos, y volver a cerrarlo

14. Notas de ampliación

Este despliegue cubre una instalación funcional de un solo trabajador (proceso), suficiente para pruebas y demos. Para un uso en producción con varios usuarios simultáneos, conviene ampliar con:

  • Múltiples workers (workers = N en odoo.conf), para que Odoo atienda varias peticiones a la vez en vez de una sola; requiere también configurar un puerto adicional de longpolling para el chat y las notificaciones en tiempo real.
  • Dominio propio + Let's Encrypt en vez de certificado autofirmado, si el servicio va a ser accesible desde fuera de la red local.
  • Importar el certificado en el almacén de confianza de cada equipo cliente, para que el aviso de "certificado no confiable" desaparezca de forma permanente.
  • Copias de seguridad del volumen de PostgreSQL y del volumen odoo-web-data (contiene los archivos adjuntos y documentos subidos, no solo la base de datos).
  • Módulos personalizados colocados en ./addons, para extender Odoo con funcionalidad propia del cliente.