Saltar al contenido
openplate

La aplicación

Autoalojamiento

Guías paso a paso de Compose, la primera cuenta, ejecución sin Docker, primera ejecución, HTTPS, copias de seguridad, actualizaciones

Esta página es una traducción automática de la documentación en inglés.

openplate se distribuye como una imagen de Docker multiarquitectura precompilada (linux/amd64 + linux/arm64; los dispositivos tipo Raspberry Pi son un objetivo de primer nivel) publicada en GitHub Container Registry. La etiqueta latest es la versión más reciente. Cada versión tiene también su propia etiqueta de versión. La etiqueta main sigue cada cambio en la rama main. No hay secretos que generar ni registros requeridos aparte de tu propia clave del proveedor de IA.

Nada aquí es una versión reducida. Registro local prioritario, escaneo de platos con IA vía BYOK, instalación como PWA, exportación e importación en JSON, toda la interfaz: idéntico a la instancia alojada, porque es la misma imagen. Lo único que añade el despliegue alojado es que gestionamos el servicio opcional de sincronización por ti, y ese servicio también es de código abierto, para que lo ejecutes tú mismo.

Qué puedes ejecutar

  • La app por sí sola. Añade un contenedor. Obtienes una configuración rápida sin base de datos ni secretos que gestionar. Te arriesgas a perder tu diario si limpias el navegador, no hay sincronización entre dispositivos y los escaneos requieren una clave de IA en la nube.
  • La app más el servidor central. Añade openplate-core y Postgres. Obtienes sincronización cifrada entre dispositivos y una factura de IA compartida opcional. Te arriesgas a perder datos si no haces copias de seguridad de la base de datos, del secreto y de tu clave de recuperación.
  • La app más inferencia autoalojada. Añade openplate-inference. Obtienes escaneos de fotos del plato en local, sin cuenta en la nube y sin que las fotos salgan de tu red. Te arriesgas a sobrecargar el hardware, y cada navegador debe poder acceder directamente al contenedor de inferencia.
  • Todo. Sincronización e inferencia juntas, cuatro contenedores en total. Obtienes privacidad total de los datos con sincronización multidispositivo. Te arriesgas al mayor mantenimiento operativo y a la mayor carga de recursos.

Cada variante es un archivo compose bajo docker/topologies/; topologies.md explica cómo elegir.

Antes de empezar

Instala Docker. Un servidor nuevo no lo tiene instalado. Sigue la guía de Docker para tu distribución en docs.docker.com/engine/install. En Ubuntu 24.04, los paquetes de la distribución también funcionan:

bash
sudo apt-get update
sudo apt install docker.io docker-compose-v2
sudo usermod -aG docker "$USER"   # then log out and back in, to use docker without sudo

Podman también funciona. Lee podman.md para ver las diferencias.

Elige una carpeta permanente. Cada guía paso a paso a continuación empieza con mkdir -p ~/openplate && cd ~/openplate. El archivo compose y su .env residen allí. Ejecuta cada comando posterior desde ahí, incluidas las actualizaciones, las copias de seguridad y los registros. No uses /tmp. Un reinicio puede vaciarlo, y tu .env con sus secretos desaparecerá.

Si ejecutas esto para tu familia, haz dos cosas cuanto antes. - Haz una copia de seguridad de .env y de la base de datos. Copia .env a un lugar seguro, sobre todo la línea SERVER_SECRET. Sin ella, una base de datos restaurada no abre ninguna cuenta. Luego programa copias de seguridad de la base de datos. Copias de seguridad tiene los comandos. - Permite que otros dispositivos accedan únicamente al puerto HTTPS. Muchos servidores arrancan sin cortafuegos, por lo que cada puerto que publica un contenedor queda abierto a tu red. Mantén los puertos del contenedor en 127.0.0.1, como muestra la sección HTTPS, para que solo tu proxy inverso acceda a ellos. Un cortafuegos como ufw no los cierra por ti, porque Docker publica sus puertos ignorándolo. ufw sigue cerrando todo lo demás en el servidor. Permite SSH primero, luego HTTPS: ``bash sudo ufw allow OpenSSH sudo ufw allow 443/tcp sudo ufw allow 8443/tcp # the core server's HTTPS port, in the recipes below sudo ufw enable ` With a domain name, Caddy also needs port 80 for its certificate: sudo ufw allow 80/tcp`.

La aplicación por sí sola

Herramienta de contenedores
mkdir -p ~/openplate && cd ~/openplate
curl -O https://raw.githubusercontent.com/LowCarbCheck/openplate/main/docker/compose.yml
docker compose -f compose.yml up -d
Ábrelo como http://localhost:3000 en el propio servidor, o a través de HTTPS. Desde otro dispositivo, http://<the server's address>:3000 muestra el diario. Instalar la aplicación, el uso sin conexión y la conexión en un clic con OpenRouter no funcionan ahí. Consulta HTTPS.

Podman ejecuta estos archivos con podman compose. En Ubuntu, ese subcomando necesita el paquete podman-compose instalado como su proveedor: consulta podman.md.

Obtienes un contenedor, ninguna base de datos, ningún paso de .env y ningún secreto que generar. La aplicación es accesible en http://localhost:3000.

El puerto se publica en la interfaz todas las. La aplicación es accesible en la dirección LAN de esta máquina en el momento en que up -d responde. En una red compartida, cambia la línea ports: a '127.0.0.1:3000:3000' y accede a ella mediante un proxy inverso.

TRUST_PROXY define cuántos proxies inversos se encuentran delante de la aplicación. El archivo compose usa 1 por defecto. Esto es correcto detrás de un proxy como Caddy o nginx. Sin él, la comprobación CSRF de la aplicación ve la dirección incorrecta y los envíos de formularios fallan. Si no hay ningún proxy delante, establécelo en 0:

bash
echo "TRUST_PROXY=0" >> .env
docker compose -f compose.yml up -d

Sin proxy, las páginas funcionan con cualquiera de los dos valores. Sin embargo, 1 permite que un visitante falsifique su dirección en X-Forwarded-For y eluda el límite por dirección en las búsquedas de alimentos.

Construye la imagen tú mismo

Para compilar desde el código fuente en lugar de descargar la imagen publicada, comenta image: en docker/compose.yml, descomenta build: y ejecuta esto desde la raíz del repositorio, ya que el contexto de compilación es relativo a ese archivo:

Herramienta de contenedores
docker compose --project-directory . -f docker/compose.yml build
docker compose --project-directory . -f docker/compose.yml up -d

Para ejecutar sin ningún contenedor, consulta Sin Docker.

Cualquier otra configuración (sincronización, inferencia autoalojada, o ambas) es un archivo independiente bajo docker/topologies/. Consulta topologies.md para elegir una.

La app más tu propio servidor central

docker/topologies/compose.core.yml es el despliegue de referencia para la app, el servidor central y la base de datos Postgres que necesita sincronización. La app sigue sin conectarse a ninguna base de datos propia. (Si también quieres inferencia autoalojada, consulta La aplicación junto con sincronización y runtime de inferencia autoalojado más abajo. Esa configuración usa la misma configuración de sincronización, más el runtime del modelo).

bash
mkdir -p ~/openplate && cd ~/openplate
curl -O https://raw.githubusercontent.com/LowCarbCheck/openplate/main/docker/topologies/compose.core.yml

# The core server needs exactly one secret. Generate it and keep it with your backups.
echo "SERVER_SECRET=$(openssl rand -hex 32)" >> .env

# Your key to the admin API. You need it to create the first account.
echo "ADMIN_TOKEN=$(openssl rand -hex 32)" >> .env

# The URLs a BROWSER will use to reach each service. Skip these two for a test
# on this machine, or through the ssh tunnel in the HTTPS section.
echo "PUBLIC_APP_URL=https://openplate.example.com" >> .env
echo "PUBLIC_SYNC_URL=https://sync.example.com" >> .env

# 1 behind one reverse proxy, 0 with none.
echo "TRUST_PROXY=1" >> .env

docker compose -f compose.core.yml up -d
bash
mkdir -p ~/openplate && cd ~/openplate
curl -O https://raw.githubusercontent.com/LowCarbCheck/openplate/main/docker/topologies/compose.core.yml

echo "SERVER_SECRET=$(openssl rand -hex 32)" >> .env
echo "ADMIN_TOKEN=$(openssl rand -hex 32)" >> .env

echo "PUBLIC_APP_URL=https://openplate.example.com" >> .env
echo "PUBLIC_SYNC_URL=https://sync.example.com" >> .env
echo "TRUST_PROXY=1" >> .env

podman compose -f compose.core.yml up -d
Las cuentas necesitan una página segura. Iniciar sesión, registrarse y abrir un enlace de invitación fallan en http://<the server's address> simple: el navegador bloquea la criptografía que utilizan. Sirve ambas direcciones a través de HTTPS, o pruébalo mediante localhost. Consulta HTTPS.

Lee el README de openplate-core antes de ejecutar esa última línea en una máquina accesible por otras personas. Ambos servicios publican sus puertos en todas las interfaces. El servicio de cuentas queda expuesto en cuanto arranca, y administrar un servicio de cuentas implica más trabajo que ejecutar la app.

El archivo está comentado línea por línea, incluidos los dos ajustes que causan problemas si se configuran mal (SERVER_SECRET y TRUST_PROXY). TRUST_PROXY se aplica a ambos servicios, ya que están tras el mismo proxy o no usan ninguno. El archivo pasa a los contenedores cada variable que los servicios leen desde .env. environment-variables.md las enumera todas. Consulta sync.md para ver qué es la sincronización y cómo accede a ella el cliente.

Crea la primera cuenta

Nadie puede registrarse por su cuenta. Las cuentas se crean al abrir una invitación enviada a una dirección de correo concreta. Genera la primera invitación para ti en el servidor con ADMIN_TOKEN. Ejecuta esto en ~/openplate con tu propia dirección. En el puerto 3001 es donde compose.core.yml y compose.full.yml publican el servidor central. El archivo compose propio del servidor central en apps/core usa el puerto 3000:

bash
ADMIN_TOKEN=$(grep '^ADMIN_TOKEN=' .env | cut -d= -f2)
curl -s -X POST http://127.0.0.1:3001/v1/admin/invites \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","displayName":"You","role":"admin"}'

La respuesta es una línea de JSON. La parte relevante tiene este aspecto:

json
"emailed":false,"link":"https://openplate.example.com/join#server=https%3A%2F%2Fsync.example.com&invite=si_..."
  • Sin correo configurado (la opción predeterminada): "emailed": false. No se escribió a nadie. Copia el link y ábrelo tú mismo.
  • Correo configurado (mira Correo): "emailed": true. El mismo enlace va de camino a esa dirección como una carta.

Abre el enlace en un navegador en una página segura, elige una contraseña y la cuenta quedará creada. Un teléfono u otro dispositivo solo podrá iniciar sesión después de que configures HTTPS, porque el túnel ssh y localhost dan servicio a un solo ordenador. El enlace funciona una sola vez y caduca a los siete días. "role":"admin" convierte esta primera cuenta en administradora. A partir de aquí, invitas a otras personas desde la propia app en /admin. En una instancia sin correo, la app muestra cada enlace nuevo. Omite role si se trata de un miembro normal. Correo explica ambas opciones: pasar cada enlace a mano o dejar que el servidor central lo envíe por correo.

En una instancia gestionada, donde el servidor central paga los escaneos de todos, añade "dailyAiLimit":200 al cuerpo para otorgar a la cuenta 200 peticiones de IA al día. El valor por defecto es 0. Una instancia gestionada necesita cuatro líneas más en .env. Los escaneos no empezarán sin un modelo, ya que la aplicación no elegirá un modelo a tu costa:

bash
echo "INSTANCE_MODE=managed" >> .env
echo "UPSTREAM_BASE_URL=https://openrouter.ai/api/v1" >> .env
echo "UPSTREAM_API_KEY=sk-or-..." >> .env
echo "AI_ADVERTISED_MODEL=vendor/model-name" >> .env

Sustituye vendor/model-name por el modelo de tu proveedor, escrito exactamente como lo escribe el proveedor. En lugar de AI_ADVERTISED_MODEL puedes definir AI_TIERS_FILE=bundled, y el servidor central tomará el modelo de su archivo de niveles, ai-tiers.json. Consulta configuration.md.

Cuando alguien olvida su contraseña

Con el correo configurado, Olvidé mi contraseña en la app envía un enlace de restablecimiento y el diario vuelve a estar disponible tras completarlo. Sin correo, la app no puede enviar nada y la página de recuperación indica al usuario que consulte al administrador. El enlace lo generas tú:

  • En la app: Administración, en Personas, abre la persona y elige Enviar un enlace de restablecimiento. Si no hay correo configurado, la página muestra el enlace. Compártelo del mismo modo que compartirías una contraseña.
  • En el servidor: busca el id de la cuenta y luego pide un enlace para ella. El puerto vuelve a ser el 3001, como en los archivos compose de topología.
bash
ADMIN_TOKEN=$(grep '^ADMIN_TOKEN=' .env | cut -d= -f2)
curl -s http://127.0.0.1:3001/v1/admin/accounts -H "Authorization: Bearer $ADMIN_TOKEN"
curl -s -X POST http://127.0.0.1:3001/v1/admin/accounts/1/reset-mail \
  -H "Authorization: Bearer $ADMIN_TOKEN"

La segunda llamada responde {"emailed":false,"link":"https://openplate.example.com/reset#server=...&token=sr_..."}. Un enlace de restablecimiento funciona una sola vez y caduca al cabo de una hora.

Correo

El servidor central puede enviar mensajes de invitación y de restablecimiento de contraseña. No es obligatorio. Una instancia familiar funciona sin configurar el correo, y es la vía más sencilla.

Sin correo

No definas ningún ajuste de correo. El servidor central no enviará mensajes. Te mostrará cada enlace a ti y tú lo compartirás como compartirías una contraseña.

  • Una invitación: abre Administración en /admin y elige Invitar a alguien. Introduce la dirección y elige Enviar la invitación. La página indica Invitación lista para esa dirección y muestra el enlace. Elige Copiar el enlace y envíaselo a esa persona, por ejemplo mediante un mensaje privado. Quien tenga el enlace puede abrir la cuenta.
  • Una contraseña olvidada: bajo Personas, entra en la persona y elige Enviar un enlace de restablecimiento. La página muestra el enlace. Pásalo de la misma manera. Funciona una sola vez, durante una hora.
  • Una invitación perdida: bajo Invitaciones, elige Enviar de nuevo junto a la dirección. Esto genera un enlace nuevo e invalida el anterior. La página muestra el enlace nuevo para copiarlo. Elige Volver a la lista para volver a tus invitaciones pendientes.

Si el enlace usa una dirección distinta a la de tu navegador, verás una advertencia debajo. Asigna a PUBLIC_APP_URL y a PUBLIC_SYNC_URL las direcciones que use tu familia y genera el enlace de nuevo. La página también te avisa si el enlace abre la página pero dirige la app a un servidor central en localhost o a una dirección http:// simple a la que otros dispositivos no pueden llegar. Asigna a PUBLIC_SYNC_URL la dirección https:// que use tu familia y genera el enlace de nuevo.

OPEN_SIGNUP=true permite que personas desconocidas soliciten una cuenta. El servidor central no arrancará con ese ajuste si no hay correo configurado. Una instancia familiar lo mantiene desactivado, y así sigue hasta que lo configures. Registro con Turnstile explica el ajuste y su captcha.

SMTP

Cualquier cuenta de correo estándar puede enviar las cartas mediante SMTP. Añade estas líneas a .env:

  • SMTP_HOST: el nombre del servidor, sin esquema ni puerto.
  • SMTP_PORT: 587 si lo omites.
  • SMTP_USER y SMTP_PASSWORD: los datos de inicio de sesión. Define ambos o déjalos vacíos para un servidor que no requiera autenticación.
  • SMTP_FROM: la dirección del remitente, como dirección simple o Name <address>.
  • MAIL_OPERATOR_EMAIL: tu propia dirección. Recibe tu copia en cancelaciones o desistimientos. Ambos transportes la exigen.

El puerto determina el modo de cifrado. El puerto 465 usa TLS desde el inicio. Cualquier otro puerto debe actualizarse mediante STARTTLS, y el servicio no envía mensajes a un servidor que no lo tenga. El texto sin cifrar solo se permite cuando SMTP_HOST es una dirección de loopback como localhost, para un receptor local como Mailpit (consulta Pruebas con Mailpit). El servicio siempre verifica los certificados.

Una cuenta de Gmail necesita una contraseña de aplicación. Google solo genera una para cuentas con la verificación en dos pasos activada. Ajustes de SMTP de Google indica smtp.gmail.com y el puerto 587:

bash
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=family.openplate@gmail.com
SMTP_PASSWORD="the app password"
SMTP_FROM="openplate <family.openplate@gmail.com>"
MAIL_OPERATOR_EMAIL=you@example.org

Amazon SES necesita credenciales de SMTP creadas para SES, que difieren de tu clave de acceso de AWS, y una dirección de remitente verificada. El host nombra tu región de AWS. Consulta la lista de endpoints para más detalles. Mientras tu cuenta permanezca en el entorno de pruebas de SES, SES solo entregará a direcciones de destinatarios verificadas.

bash
SMTP_HOST=email-smtp.eu-central-1.amazonaws.com
SMTP_PORT=587
SMTP_USER=<SES SMTP user name>
SMTP_PASSWORD=<SES SMTP password>
SMTP_FROM="openplate <noreply@example.org>"
MAIL_OPERATOR_EMAIL=you@example.org

Vuelve a crear el servidor central con docker compose -f <your file> up -d tras cualquier cambio en .env.

Una API de correo HTTP

Un servicio de correo con API HTTP también funciona. Define MAIL_API_URL, MAIL_API_KEY y MAIL_API_FROM, los tres, más MAIL_OPERATOR_EMAIL. El servidor central envía cada mensaje como una petición JSON POST a MAIL_API_URL, con MAIL_API_KEY como token Bearer, siguiendo el formato que espera la API de Resend. Resend es un servicio compatible. En Resend, MAIL_API_URL es https://api.resend.com/emails.

Configura un solo transporte. Si defines tanto SMTP como la API de correo, el servidor central no arrancará.

El correo necesita las direcciones públicas

Cada mensaje incluye un enlace, y ese enlace debe abrirse en el teléfono de quien lo reciba. Cuando configures SMTP o una API de correo, asigna a PUBLIC_APP_URL y a PUBLIC_SYNC_URL las direcciones https:// que use tu familia. Si alguno de los ajustes usa http:// simple o una dirección de bucle invertido como localhost, el servidor central no arrancará. Su registro indicará cada valor que debes corregir, en un mensaje que empieza así:

Mail is configured. Its messages would carry links that recipients cannot open.

El mensaje del registro llama a estos valores CLIENT_BASE_URL y SERVER_PUBLIC_URL. Esos son los nombres internos que lee el servidor central, y los archivos compose los asignan desde PUBLIC_APP_URL y PUBLIC_SYNC_URL. Consulta el mensaje con docker compose -f <your file> logs core.

Comprueba que el correo funcione

Envía una invitación a una segunda dirección tuya en /admin. La página debería mostrar Invitación enviada a esa dirección y el mensaje debería llegar a tu bandeja de entrada. Si la página muestra Invitación lista para y escribe un enlace, el envío falló. El enlace sigue siendo válido. Revisa el registro del servidor central buscando una línea Mail send failed para ver el motivo. Cuando termines las pruebas, elige Retirar para la invitación de prueba en Invitaciones.

Pruebas con Mailpit

Mailpit intercepta cada mensaje y lo muestra en una página web, para que pruebes el correo sin tener una cuenta de correo. Ejecútalo como un sidecar que comparta la red del contenedor central. El servidor central llegará a él como localhost, donde se permite texto plano. Guarda este archivo junto a tu archivo compose como compose.mailpit.yml:

yaml
# compose.mailpit.yml: a mail catcher for testing, next to your compose file
services:
  core:
    ports:
      - '127.0.0.1:8025:8025' # Mailpit's web page, on this machine only
  mailpit:
    image: docker.io/axllent/mailpit:latest
    restart: unless-stopped
    network_mode: 'service:core'

Añade estas líneas a .env. Mailpit recibe mensajes en el puerto 1025:

bash
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_FROM="openplate <test@example.org>"
MAIL_OPERATOR_EMAIL=you@example.org

Inicia ambos archivos a la vez, luego envía una invitación y léela en http://localhost:8025 en el servidor:

bash
docker compose -f compose.core.yml -f compose.mailpit.yml up -d

La regla de El correo necesita las direcciones públicas sigue aplicando. Asigna primero direcciones https:// a PUBLIC_APP_URL y PUBLIC_SYNC_URL, o el servidor central no arrancará.

Un contenedor independiente de Mailpit al que se acceda por nombre de servicio, como SMTP_HOST=mailpit, no funciona. El servidor central solo envía texto plano a esta máquina. Un nombre de servicio cuenta como otro host. El servicio solicita STARTTLS, Mailpit no lo ofrece y todos los mensajes fallan con Mail send failed en el registro. La red compartida sitúa a Mailpit en la misma máquina.

Cuando termines las pruebas, elimina las cuatro líneas de .env. Luego desecha Mailpit con docker compose -f compose.core.yml up -d --remove-orphans.

Un relay con una autoridad de certificación privada

El servidor central comprueba el certificado de cada servidor de correo. Un relay dentro de una red corporativa puede usar un certificado firmado por una entidad de certificación privada. Node.js no confía en esa entidad de forma predeterminada. Pasa al servidor central el certificado de esa entidad como archivo PEM. Monta el archivo dentro del contenedor y asigna su ruta a NODE_EXTRA_CA_CERTS. Por ejemplo, en un compose.ca.yml junto a tu archivo compose:

yaml
# compose.ca.yml: trust a private certificate authority for the mail relay
services:
  core:
    volumes:
      - ./relay-ca.pem:/etc/openplate/relay-ca.pem:ro
bash
echo "NODE_EXTRA_CA_CERTS=/etc/openplate/relay-ca.pem" >> .env
docker compose -f compose.core.yml -f compose.ca.yml up -d

Node.js lee el archivo una vez, al arrancar. El certificado se añade a los que Node.js ya considera de confianza, por lo que los servidores de correo públicos siguen funcionando.

La app junto con el runtime de inferencia en Autoalojamiento

docker/topologies/compose.inference.yml ejecuta la app junto con openplate-inference. Las fotos del plato se procesan en tu propio hardware y cada visitante recibe un aviso de un solo toque: «este openplate proporciona su propia IA». Lee primero la sección de hardware en topologies.md. El modelo pequeño lite requiere alrededor de 1,6 GB de RAM y entre unos segundos y un minuto por plato en una CPU.

bash
mkdir -p ~/openplate && cd ~/openplate
curl -O https://raw.githubusercontent.com/LowCarbCheck/openplate/main/docker/topologies/compose.inference.yml

# One key, generated here on the server. The inference service accepts it and
# the app hands it to every browser.
echo "INFERENCE_API_KEY=opk_$(openssl rand -hex 24)" >> .env

# The two URLs a BROWSER will use. Replace 192.168.1.20 with this machine's
# address, or with the names your reverse proxy serves.
echo "PUBLIC_APP_URL=http://192.168.1.20:3000" >> .env
echo "PUBLIC_INFERENCE_URL=http://192.168.1.20:8300/v1" >> .env

# 0 with no reverse proxy, 1 behind one.
echo "TRUST_PROXY=0" >> .env

docker compose -f compose.inference.yml up -d
docker compose -f compose.inference.yml logs -f inference
HTTP sin cifrar funciona para los escaneos, HTTPS requiere HTTPS en todo el trayecto. Con http://<the server's address> sin cifrar, una foto del plato llega al contenedor de inferencia, pero instalar la app no funciona. En cuanto la app funciona sobre https://, la dirección de inferencia debe ser https:// también. De lo contrario, el navegador bloquea la llamada desde la página segura. Consulta HTTPS.

PUBLIC_INFERENCE_URL debe ser una dirección que un navegador pueda abrir, porque la foto viaja directamente desde el teléfono hasta el contenedor de inferencia. http://inference:8300/v1, el nombre con el que los contenedores se comunican entre sí, no sirve aquí. Conserva el /v1 al final.

El primer inicio descarga aproximadamente 2 GiB de pesos (1,96 GiB) en un volumen con nombre, proceso que duró entre seis y siete minutos en nuestras pruebas, y después carga el modelo. El registro muestra cada paso. La app queda disponible de inmediato; la IA de un solo toque funciona en cuanto esto responde 200:

bash
curl -s http://127.0.0.1:8300/readyz

La clave está en la página que carga cada navegador, por lo que cualquiera que pueda abrir la app puede leerla. Eso no da problemas en una red doméstica o en una tailnet, pero no es adecuado en una instancia abierta a internet. configuration.md contiene la regla completa. El contenedor de inferencia usa todos los núcleos de la CPU menos dos; define LLAMA_THREADS en .env para cambiar esto.

La aplicación junto con sincronización y runtime de inferencia autoalojado

docker/topologies/compose.full.yml ejecuta cuatro contenedores: la app, el servidor central, Postgres y la inferencia autoalojada. Lee La app más tu propio servidor central y La app junto con el runtime de inferencia en Autoalojamiento primero. Esta sección solo cubre los cambios que surgen cuando todas las piezas funcionan juntas.

bash
mkdir -p ~/openplate && cd ~/openplate
curl -O https://raw.githubusercontent.com/LowCarbCheck/openplate/main/docker/topologies/compose.full.yml

# The core server needs exactly one secret. Generate it and keep it with your backups.
echo "SERVER_SECRET=$(openssl rand -hex 32)" >> .env

# Your key to the admin API. You need it to create the first account.
echo "ADMIN_TOKEN=$(openssl rand -hex 32)" >> .env

# One key for the inference service, which the app hands to every browser.
echo "INFERENCE_API_KEY=opk_$(openssl rand -hex 24)" >> .env

# The URLs a BROWSER will use to reach each service. PUBLIC_APP_URL and
# PUBLIC_SYNC_URL default to localhost, so skip both for a test on this
# machine. PUBLIC_INFERENCE_URL has no such default: set it even for a
# local test, for example to http://localhost:8300/v1.
echo "PUBLIC_APP_URL=https://openplate.example.com" >> .env
echo "PUBLIC_SYNC_URL=https://sync.example.com" >> .env
echo "PUBLIC_INFERENCE_URL=https://ai.example.com/v1" >> .env

# 1 behind one reverse proxy, 0 with none.
echo "TRUST_PROXY=1" >> .env

docker compose -f compose.full.yml up -d

Crea la primera cuenta igual que en más arriba. El primer inicio también descarga los pesos de inferencia, unos 2 GiB. El registro de carga del modelo y la comprobación de disponibilidad funcionan igual que en La app junto con el runtime de inferencia en Autoalojamiento. Para usar dispositivos reales en vez de hacer una prueba, pon las tres direcciones tras HTTPS. Consulta HTTPS.

Sin Docker

La app es un único programa de Node.js. Se ejecuta directamente desde una copia del repositorio. Aquí se explica cómo ejecutarla en Ubuntu 24.04 con systemd manteniéndola activa tras cierres de sesión y reinicios.

Node.js 24 o más reciente. Ubuntu 24.04 proporciona nodejs versión 18, que es demasiado antigua. Instala la versión 24 desde NodeSource:

bash
curl -fsSL https://deb.nodesource.com/setup_24.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt install -y nodejs git
node --version      # v24.x

nvm también funciona, pero instala Node en tu carpeta personal. La unidad de systemd que figura más abajo debe usar entonces esa ruta (command -v node la muestra).

pnpm, la versión que pide el repositorio. corepack viene con Node 24. Descarga la versión exacta de pnpm configurada en el campo packageManager del archivo package.json de la app, pero solo dentro de apps/app. Fuera de esa carpeta, pnpm usa por defecto lo que elija corepack. Ejecuta cada comando de pnpm dentro de apps/app. En la primera ejecución, confirma la solicitud de descarga.

bash
sudo corepack enable
git clone https://github.com/LowCarbCheck/openplate.git ~/openplate-src
cd ~/openplate-src/apps/app
pnpm install --frozen-lockfile
pnpm build

La configuración. El servidor lee .env desde la carpeta donde se ejecuta. En producción no arrancará sin APP_URL:

bash
cat > .env <<'EOF'
NODE_ENV=production
PORT=3000
APP_URL=http://localhost:3000
TRUST_PROXY=0
EOF

Establece APP_URL con la dirección que abre la gente, y TRUST_PROXY=1 una vez que haya un proxy inverso delante. Cada una de las demás variables de la app va en el mismo archivo. HOST=127.0.0.1 hace que el servidor escuche solo en esta máquina, que es lo que buscas tras un proxy en el mismo equipo.

Una unidad de systemd, para que la app arranque con el sistema y siga funcionando tras cerrar sesión. Las variables $USER y $HOME de abajo se rellenan al pegar el texto:

bash
sudo tee /etc/systemd/system/openplate.service > /dev/null <<EOF
[Unit]
Description=openplate
After=network-online.target
Wants=network-online.target

[Service]
User=$USER
WorkingDirectory=$HOME/openplate-src/apps/app
Environment=NODE_ENV=production
ExecStart=/usr/bin/node --import tsx ./server.ts
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now openplate
curl -s http://127.0.0.1:3000/healthcheck

sudo journalctl -u openplate -f muestra el registro. Para actualizar, descarga los cambios con pull, compila otra vez y luego reinicia:

bash
cd ~/openplate-src/apps/app
git pull
pnpm install --frozen-lockfile
pnpm build
sudo systemctl restart openplate

La sección HTTPS se aplica aquí sin cambios: apunta el proxy inverso al puerto 3000.

Primera ejecución

  1. Abre la app y sigue la breve introducción inicial. Sin registro ni inicio de sesión: quien abre la app en un dispositivo es el usuario de ese dispositivo.
  2. Ve a Ajustes → IA y conecta un proveedor de IA con tu propia clave de API (OpenRouter, Mistral, tu propio endpoint compatible con OpenAI o Anthropic). Consulta configuration.md para el flujo en un clic de OpenRouter y para ofrecer en su lugar un endpoint provisto por la instancia.
  3. Haz una copia de seguridad cuanto antes: Ajustes → Datos y copia de seguridad → Descargar todo (JSON). Tu diario reside en el almacenamiento de este navegador, así que una copia de seguridad es la única forma de conservarlo si borras los datos del sitio o cambias de dispositivo.
  4. Si más de una persona escanea en esta instancia, obtén una clave gratuita para la base de datos de alimentos en lowcarbcheck.org/developers, añádela a .env como FOOD_DB_API_KEY=... y vuelve a ejecutar docker compose -f <your file> up -d. Sin una clave, todas las personas de la instancia comparten una cuota anónima reducida. Consulta configuration.md. Con una clave también puedes definir FOOD_DB_BACKFILL=true, lo que envía a LowCarbCheck, como propuestas, los alimentos que los usuarios guarden a partir de una respuesta de la IA. Consulta configuration.md.

HTTPS

Los navegadores limitan varias funciones a un contexto seguro. Un contexto seguro es una página servida mediante https:// o desde localhost en la máquina local. openplate requiere un contexto seguro para varias funciones:

  • Cuentas, inicio de sesión y sincronización. Iniciar sesión, registrarse y abrir un enlace de invitación o de restablecimiento derivan claves mediante la Web Crypto API (crypto.subtle). Los navegadores desactivan esta API en páginas http:// normales. En http://192.168.1.20:3000, estas pantallas dan error. La función de compartir y la consola de investigación también fallan, porque usan la misma API.
  • Conectar con OpenRouter, el inicio de sesión en un solo clic con OpenRouter, por la misma razón. Pegar una clave a mano funciona en cualquier sitio.
  • Instalar la app y uso sin conexión (el service worker).

Por HTTP simple desde otro dispositivo, el diario, el registro manual, las copias de seguridad y las fotos del plato siguen funcionando. El botón de la foto recurre a la propia cámara del teléfono mediante un selector de archivos, lo cual no necesita una página segura. La guía de inicio rápido funciona sin cambios en el propio servidor, ya que localhost cuenta como seguro.

Tus dispositivos necesitan una dirección segura. Puedes configurar una de cuatro formas: un túnel ssh para una prueba rápida, Caddy con un nombre de dominio, Caddy en una red doméstica sin nombre de dominio, o Tailscale.

Una prueba rápida desde un ordenador: un túnel ssh

Esto es una prueba para un ordenador, no una instalación definitiva. El túnel solo sirve al ordenador que lo ejecuta, y solo mientras el comando esté en ejecución. Un teléfono u otro dispositivo no pueden iniciar sesión a través de él. Para ellos, configura HTTPS con Caddy más abajo.

Para probar cuentas y sincronización antes de configurar un certificado, redirige los dos puertos a tu ordenador. No definas ni PUBLIC_APP_URL ni PUBLIC_SYNC_URL para que ambos conserven sus valores predeterminados en localhost. Tampoco configures el correo aquí. Con el correo configurado, el servidor central no arrancará mientras las direcciones de sus enlaces indiquen localhost. Sin correo, arrancará y copiarás tú cada enlace. Ejecuta este comando en tu ordenador, no en el servidor:

bash
ssh -N -L 3000:localhost:3000 -L 3001:localhost:3001 you@192.168.1.20

Mientras el comando esté en ejecución, abre http://localhost:3000 en tu navegador. Esto cuenta como una página segura, por lo que iniciar sesión funciona. Un enlace de invitación del servidor (http://localhost:3000/join#...) también se abre ahí. Añade -L 8300:localhost:8300 para el contenedor de inferencia.

Un nombre de dominio: Caddy

Caddy obtiene y renueva un certificado de Let's Encrypt automáticamente. Requiere un nombre de dominio que apunte a tu servidor. También necesita que los puertos 80 y 443 sean accesibles desde internet para verificar el certificado.

# Caddyfile
openplate.example.com {
    reverse_proxy localhost:3000
}

A continuación, define APP_URL=https://openplate.example.com en .env. Establece TRUST_PROXY=1, que es el valor predeterminado en producción. Vuelve a crear el servicio app con docker compose -f compose.yml up -d. Un simple docker compose restart no vuelve a leer .env. Solo reinicia el contenedor existente, de modo que los nuevos valores nunca se cargan.

Para sincronizar, dale al servidor central su propio nombre de dominio. Configura ambas URL públicas en lugar de APP_URL:

# Caddyfile
openplate.example.com {
    reverse_proxy localhost:3000
}
sync.example.com {
    reverse_proxy localhost:3001
}
bash
PUBLIC_APP_URL=https://openplate.example.com
PUBLIC_SYNC_URL=https://sync.example.com
TRUST_PROXY=1

Cambia la línea de ports: a '127.0.0.1:3000:3000', y usa '127.0.0.1:3001:3000' para la sincronización. Aplica el cambio con docker compose -f <your file> up -d. Un proxy inverso no retira los puertos publicados de un contenedor. Si lo dejas como '3000:3000', la app sigue sirviendo HTTP sin cifrar en el puerto 3000 a través de tu red local junto a la dirección HTTPS.

Podman vuelve a crear el servicio del mismo modo con podman compose -f compose.yml up -d. Ten en cuenta un detalle sobre rootless antes de prescindir del proxy inverso: un contenedor de Podman sin privilegios de root no puede vincularse a un puerto del host inferior a 1024 sin configuración adicional. Publicar directamente en el puerto 80 o 443 requiere sudo sysctl net.ipv4.ip_unprivileged_port_start=80 antes. Consulta podman.md.

Sin nombre de dominio, solo red doméstica: Caddy con un certificado local

Sin un nombre de dominio, Caddy aún puede servir HTTPS en tu red doméstica. Crea su propia autoridad de certificación y la usa para firmar un certificado para la dirección del servidor. Cada teléfono y ordenador que abra openplate debe confiar en esa autoridad una vez. Tras eso, los teléfonos de la familia reciben una página segura. Iniciar sesión, la sincronización y la instalación de la app funcionan mediante https://.

Asigna una dirección fija al servidor. En tu router, reserva la dirección actual del servidor, por ejemplo 192.168.1.20, para que no cambie nunca. El certificado y las dos direcciones públicas la mencionan.

Instala Caddy y apúntalo a la dirección. En Ubuntu, sudo apt install caddy instala Caddy como un servicio. Sustituye /etc/caddy/Caddyfile por esto, usando la dirección de tu servidor. tls internal le indica a Caddy que firme el certificado por sí mismo:

# /etc/caddy/Caddyfile
https://192.168.1.20 {
    tls internal
    reverse_proxy localhost:3000
}
https://192.168.1.20:8443 {
    tls internal
    reverse_proxy localhost:3001
}
bash
sudo systemctl reload caddy

Define las mismas direcciones en .env, luego vuelve a crear los contenedores con docker compose -f <your file> up -d:

bash
PUBLIC_APP_URL=https://192.168.1.20
PUBLIC_SYNC_URL=https://192.168.1.20:8443
TRUST_PROXY=1

Para la aplicación por sí sola, define APP_URL=https://192.168.1.20 en su lugar. Omite el segundo bloque del Caddyfile. Como en la receta anterior, cambia las líneas de ports: por '127.0.0.1:3000:3000' y '127.0.0.1:3001:3000', para que solo Caddy acceda a los contenedores.

Copia el certificado raíz de Caddy fuera del servidor. El Caddy de Ubuntu lo guarda en /var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt. El certificado es público. Su clave privada está en la misma carpeta y nunca debe salir del servidor, así que copia únicamente root.crt:

bash
sudo cp /var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt ~/openplate-root.crt
sudo chown "$USER" ~/openplate-root.crt

En tu ordenador, descárgalo con scp you@192.168.1.20:openplate-root.crt .. Luego envíalo a cada teléfono, por ejemplo en un correo a ti mismo o con AirDrop.

Confía en él en cada dispositivo, una sola vez. Este es el paso que hace que los teléfonos de la familia funcionen mediante https://.

  • iPhone e iPad: abre el archivo y permite la descarga. En Ajustes, pulsa Perfil descargado cerca de la parte superior, o búscalo en General > Gestión de VPN y dispositivos, e instálalo. Luego activa la confianza plena en Ajustes > General > Información > Ajustes de confianza de certificados. Sin este último paso, el navegador seguirá rechazando la página.
  • Android: guarda el archivo en el teléfono. Abre Ajustes > Seguridad > Cifrado y credenciales > Instalar un certificado > Certificado CA, acepta la advertencia y elige el archivo. En teléfonos más modernos la ruta empieza en Seguridad y privacidad > Más ajustes de seguridad, y los nombres varían un poco según el fabricante. Si el teléfono no tiene bloqueo de pantalla, Android te pedirá configurar uno.
  • Un ordenador: añádelo a los certificados del sistema. Algunos navegadores mantienen su propia lista y necesitan tenerlo ahí también.

Abre https://192.168.1.20 en un teléfono. La página debería cargar sin advertencias, y el inicio de sesión debería funcionar. La dirección solo funciona en tu red local. Guarda la carpeta de datos de Caddy junto a tus copias de seguridad. Una nueva instalación de Caddy crea una autoridad nueva, y cada dispositivo tendrá que confiar en la nueva.

Cualquier otro proxy inverso

nginx, Traefik u otro proxy pueden sustituir a Caddy. No hemos probado ninguno de ellos, así que esto es una lista de comprobación, no una receta. El proxy debe hacer todo esto:

  • Dos direcciones https://. La app y el servidor central reciben cada uno el suyo.
  • Las mismas direcciones en .env. Configura PUBLIC_APP_URL y PUBLIC_SYNC_URL exactamente con esas direcciones. Para la app por sí sola, eso es APP_URL.
  • TRUST_PROXY=1, o el número de proxies en la cadena.
  • Host y X-Forwarded-Proto llegan a la app. Pasa la cabecera Host del navegador sin cambios, o asigna X-Forwarded-Host a su valor. Define X-Forwarded-Proto como https. El control CSRF de la app construye la dirección de la propia página a partir de estos valores y la compara con Origin del navegador. Si son incorrectos, los envíos de formularios fallarán.
  • X-Forwarded-For llega a ambos servicios. Sus límites por dirección lo leen.
  • Los puertos del contenedor se quedan en 127.0.0.1. Mantén '127.0.0.1:3000:3000' y '127.0.0.1:3001:3000', para que nada llegue a ellos saltándose el proxy.

Sin nombre de dominio: Tailscale Serve

Tailscale da a cada máquina de tu tailnet una dirección HTTPS bajo ts.net. No necesitas nombre de dominio, ni puertos abiertos, ni gestionar certificados a mano. Tailscale Serve coloca esa dirección delante de un puerto en esta máquina. openplate con sincronización necesita dos direcciones. Das servicio a dos puertos bajo el mismo nombre de máquina: la app en el 443 y el servidor central en el 8443.

Antes de empezar:

  • Activa MagicDNS y los certificados HTTPS para tu tailnet en la página de DNS de la consola de administración de Tailscale. Guía de HTTPS de Tailscale detalla los pasos. El nombre de la máquina aparece en un registro público de certificados, así que elige un nombre que no revele nada privado.
  • Cada miembro de la familia ejecuta Tailscale en cada dispositivo que abra openplate. Cada persona debe estar en tu tailnet o tener esta máquina compartida con ellos.
  • Mantén los puertos del contenedor en 127.0.0.1: '127.0.0.1:3000:3000' para la app y '127.0.0.1:3001:3000' para la sincronización. Tailscale Serve llega a ellos en esta máquina. Nada más necesita acceso.

Luego sirve ambos puertos. --bg los mantiene ejecutándose en segundo plano, y Tailscale vuelve a servirlos tras reiniciar:

bash
tailscale serve --bg --https=443 3000
tailscale serve --bg --https=8443 3001
tailscale serve status

Tailscale emite y renueva el certificado. Configura ambas direcciones en .env, usando tus propios nombres de máquina y de tailnet:

bash
PUBLIC_APP_URL=https://<machine-name>.<tailnet>.ts.net
PUBLIC_SYNC_URL=https://<machine-name>.<tailnet>.ts.net:8443
TRUST_PROXY=1

Tailscale Serve es un proxy inverso, así que TRUST_PROXY se queda en 1. Vuelve a crear el stack con docker compose -f <your file> up -d. Si ejecutas la app por separado, sirve solo el puerto 3000 y establece APP_URL con la primera dirección.

No hemos probado esta vía de extremo a extremo. La Referencia de tailscale serve documenta los parámetros anteriores y Documentación de Tailscale describe tailscale serve. La receta de Caddy de arriba es la que hemos verificado.

Headscale, un servidor de control de Tailscale autoalojado, no emite certificados HTTPS. Una tailnet de Headscale necesita en su lugar la receta de Caddy con un dominio.

Copias de seguridad

No hay nada que respaldar en el servidor de la app. No contiene ninguna base de datos ni escribe ningún estado: destruir el contenedor de la app no pierde nada.

La exportación en JSON de cada dispositivo es la copia de seguridad importante: Ajustes → Datos y copia de seguridad → Descargar todo (JSON). Ese archivo es la copia que sobrevive si borras los datos del navegador o si el teléfono se rompe. La app muestra un aviso si un dispositivo guarda datos que nunca has exportado, o que hace tiempo que no exportas.

Mantén la exportación con la misma privacidad que el diario. Contiene cada entrada en texto claro, y también contiene la clave privada de la identidad de compartición de este dispositivo y la raíz de la que se deriva el seudónimo de investigación. Quien tenga el archivo puede abrir un diario que se haya compartido con esta persona, y puede vincular sus aportaciones al estudio con ella. Hay dos cosas que quedan fuera: la clave del proveedor de IA y las fotos del plato, que nunca salen del dispositivo.

Si también ejecutas el servidor central, conviene programar un volcado de su Postgres, junto con el SERVER_SECRET, que no sirve de nada sin la base de datos y viceversa:

Herramienta de contenedores
docker compose -f compose.core.yml exec postgres \
  pg_dump -U openplate openplate_sync > sync-backup.sql

Actualizar

Herramienta de contenedores
docker compose -f compose.yml pull
docker compose -f compose.yml up -d

Usa el mismo archivo -f que usaste al desplegar. Si levantaste una topología a partir de docker/topologies/, indica ese archivo, por ejemplo docker compose -f compose.core.yml pull. Ejecutar simplemente docker compose pull junto a compose.core.yml fallará con no configuration file provided: not found.

Los archivos de compose usan la etiqueta latest, que es la versión más reciente, así que pull te lleva a ella. Para elegir cuándo actualizar, fija una versión en la línea image:, por ejemplo ghcr.io/lowcarbcheck/openplate:0.54.0. Cambia el número cuando quieras la siguiente versión. El servidor central y el servicio de inferencia tienen sus propios números de versión, así que fija cada imagen a la suya. La etiqueta main sigue cada cambio de la rama principal. Sirve para hacer pruebas, no para un servidor que use tu familia.

No hay nada que migrar: el contenedor de la aplicación no guarda estado, por lo que una imagen nueva simplemente reemplaza a la anterior. Si ejecutas la pila completa, el servidor central aplica sus propias migraciones al arrancar.

Actualizar desde una imagen anterior a 0.1.x (una sola vez)

Las imágenes más antiguas ejecutaban un sistema de cuentas y su propio Postgres. Ya no existen ninguno de los dos. La actualización elimina la tabla users y todo lo que depende de ella: cuentas, sesiones, tokens de verificación y de restablecimiento. Nada de lo que hayas registrado se ve afectado: los datos de seguimiento ya estaban en el dispositivo. Es un cambio irreversible, así que:

  1. Haz una copia de seguridad primero. Haz un pg_dump de la base de datos antigua de la app si quieres poder recuperar las filas de las cuentas, y haz que cada persona exporte un JSON en cada dispositivo desde Perfil → Tus datos. Esa exportación es la copia que guarda su diario.
  2. Actualiza. Primero actualiza la línea image: a ghcr.io/lowcarbcheck/openplate:latest, la versión más reciente: las imágenes previas a 0.1.x se publicaban bajo ghcr.io/sprqvntrs/openplate, y descargar sin este cambio solo vuelve a traer la anterior. Luego, la migración se ejecuta al iniciar el contenedor. Después no hay página de inicio de sesión: cada dispositivo que ya tenga datos los conserva y simplemente deja de preguntar quién eres.
  3. Limpia tu .env. Las variables para el secreto de sesión, la clave de cifrado, el control de registro y el superadministrador inicial han desaparecido, al igual que DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME y las variables de ajuste del pool. Nada las lee. Dejarlas definidas no hace daño, pero son peso muerto.
  4. Elimina el volumen antiguo cuando lo veas todo correcto. Las versiones más antiguas no fijaban un nombre de proyecto en Compose, por lo que el prefijo dependía del directorio donde ejecutabas (comprueba primero el nombre real): docker volume ls | grep pg-data, luego docker compose down && docker volume rm <that name>.

Si había dos cuentas iniciadas en el mismo perfil del navegador, ten en cuenta que el almacenamiento del dispositivo siempre tuvo alcance de dispositivo: sus datos ya compartían un único almacenamiento y siguen así. Usar perfiles de navegador distintos sigue siendo la forma de separar los diarios de dos personas en un mismo dispositivo.

Edita esta página en GitHub