Salta al contenuto
openplate

L'app

Self-hosting

Guide passo passo su Compose, il primo account, esecuzione senza Docker, primo avvio, HTTPS, backup, aggiornamento

Questa pagina è tradotta automaticamente dalla documentazione in inglese.

openplate viene distribuito come immagine Docker multi-architettura precompilata (linux/amd64 + linux/arm64; i dispositivi di classe Raspberry Pi sono un target primario) pubblicata su GitHub Container Registry. Il tag latest corrisponde alla versione più recente. Ogni versione ha anche un proprio tag di versione. Il tag main segue ogni modifica sul ramo main. Non ci sono segreti da generare né registrazioni da fare, a parte la chiave del tuo provider di IA.

Nulla qui è in versione ridotta. Tracciamento local-first, scansione delle foto del piatto tramite IA con BYOK, installazione PWA, esportazione/importazione in JSON, l'intera interfaccia: tutto identico all'istanza ospitata, perché si tratta della medesima immagine. L'unica cosa che il deployment ospitato aggiunge è la gestione del servizio di sincronizzazione opzionale al posto tuo, ed è anch'esso open source, pronto per essere eseguito in autonomia.

Cosa puoi eseguire

  • L'applicazione da sola. Aggiunge un container. Ottieni una configurazione rapida senza database o segreti da gestire. Rischi di perdere il tuo diario se cancelli i dati del browser, non c'è sincronizzazione tra dispositivi e le scansioni richiedono una chiave AI sul cloud.
  • L'app con il server centrale. Aggiunge openplate-core e Postgres. Ottieni la sincronizzazione cifrata tra dispositivi e una fatturazione AI condivisa opzionale. Rischi di perdere dati se non esegui il backup del database, del segreto e della tua chiave di recupero.
  • L'applicazione insieme all'inferenza self-hosted. Aggiunge openplate-inference. Ottieni la scansione locale dei piatti senza account cloud e senza foto che lasciano la tua rete. Rischi di sovraccaricare l'hardware, e ogni browser deve raggiungere direttamente il container di inferenza.
  • Tutto. Sincronizzazione e inferenza insieme, quattro container in totale. Ottieni la privacy completa dei dati con sincronizzazione multi-dispositivo. Rischi il carico di manutenzione operativa e di risorse più alto.

Ogni combinazione corrisponde a un file compose in docker/topologies/; topologies.md spiega come scegliere.

Prima di iniziare

Installa Docker. Un server appena installato non lo include. Segui la guida di Docker per la tua distribuzione su docs.docker.com/engine/install. Su Ubuntu 24.04 funzionano anche i pacchetti della distribuzione:

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

Funziona anche Podman. Leggi podman.md per le differenze.

Scegli una cartella permanente. Tutte le guide qui sotto partono da mkdir -p ~/openplate && cd ~/openplate. Il file compose e il suo .env si trovano lì. Esegui ogni comando successivo da quella posizione, inclusi aggiornamenti, backup e log. Non usare /tmp. Un riavvio può svuotarla, facendo sparire il tuo .env con i suoi segreti.

Se configuri questo sistema per la tua famiglia, fai subito due cose. - Esegui il backup di .env e del database. Copia .env in un luogo sicuro, soprattutto la riga SERVER_SECRET. Senza di essa, un database ripristinato non apre alcun account. Poi pianifica dump periodici del database. Backup contiene i comandi necessari. - Consenti agli altri dispositivi di raggiungere solo la porta HTTPS. Molti server si avviano senza firewall, quindi ogni porta pubblicata da un container è aperta sulla tua rete. Mantieni le porte dei container su 127.0.0.1, come mostra la sezione HTTPS, in modo che solo il tuo reverse proxy possa raggiungerle. Un firewall come ufw non le chiude al posto tuo, perché Docker pubblica le sue porte a monte del firewall. ufw blocca comunque tutto il resto sul server. Consenti prima SSH, poi 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`.

L'applicazione da sola

Strumento per i container
mkdir -p ~/openplate && cd ~/openplate
curl -O https://raw.githubusercontent.com/LowCarbCheck/openplate/main/docker/compose.yml
docker compose -f compose.yml up -d
Aprilo come http://localhost:3000 sul server stesso, o tramite HTTPS. Da un altro dispositivo, http://<the server's address>:3000 mostra il diario. L'installazione dell'app, l'uso offline e la connessione a OpenRouter con un clic non funzionano lì. Vedi HTTPS.

Podman esegue questi file con podman compose. Su Ubuntu quel sottocomando richiede l'installazione del pacchetto podman-compose come fornitore: vedi podman.md.

Ottieni un solo container, nessun database, nessun passaggio di .env e nessun segreto da generare. L'app è raggiungibile su http://localhost:3000.

La porta è pubblicata sull'interfaccia ogni. L'app è raggiungibile all'indirizzo LAN di questa macchina non appena up -d restituisce il controllo. Su una rete condivisa, cambia la riga ports: in '127.0.0.1:3000:3000' e raggiungila invece tramite un reverse proxy.

TRUST_PROXY definisce quanti reverse proxy si trovano davanti all'app. Il file compose usa come valore predefinito 1. Questo è corretto dietro a un proxy come Caddy o nginx. Senza di esso, il controllo CSRF dell'app rileva l'indirizzo sbagliato e le richieste POST dei moduli falliscono. Senza proxy davanti, impostalo su 0:

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

Senza proxy, le pagine funzionano con entrambi i valori. Tuttavia, 1 consente a un visitatore di falsificare il proprio indirizzo in X-Forwarded-For e aggirare il limite per indirizzo sulle ricerche degli alimenti.

Compila l'immagine in autonomia

Per compilare dai sorgenti invece di scaricare l'immagine pubblicata, commenta image: in docker/compose.yml, decommenta build: ed esegui il comando dalla radice del repository, poiché il contesto di compilazione è relativo a quel file:

Strumento per i container
docker compose --project-directory . -f docker/compose.yml build
docker compose --project-directory . -f docker/compose.yml up -d

Per eseguire senza alcun container, vedi Senza Docker.

Tutte le altre configurazioni (sincronizzazione, inferenza in Self-hosting, o entrambe) si trovano in file separati sotto docker/topologies/. Vedi topologies.md per sceglierne una.

L'app con il tuo server centrale

docker/topologies/compose.core.yml è il deployment di riferimento per l'app, il server centrale e il database Postgres richiesto da sincronizzazione. L'app continua a non connettersi a nessun database proprio. (Se vuoi anche l'inferenza in Self-hosting, vedi L'app con sincronizzazione e inferenza in Self-hosting sotto. Tale configurazione usa la stessa impostazione di sincronizzazione, più il runtime del modello.)

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
Gli account richiedono una pagina sicura. L'accesso, la registrazione e l'apertura di un link di invito falliscono su http://<the server's address> non cifrato: il browser blocca la crittografia che utilizzano. Distribuisci entrambi gli indirizzi su HTTPS, oppure esegui i test tramite localhost. Vedi HTTPS.

Leggi il README di openplate-core prima di eseguire quell'ultima riga su una macchina accessibile da altre persone. Entrambi i servizi pubblicano le proprie porte su ogni interfaccia. Il servizio degli account viene esposto non appena si avvia, e gestire un servizio account è un impegno più grande rispetto all'esecuzione dell'app.

Il file è commentato riga per riga, incluse le due impostazioni che creano problemi se configurate male (SERVER_SECRET e TRUST_PROXY). TRUST_PROXY si applica a entrambi i servizi, perché si trovano dietro lo stesso proxy o non ne hanno alcuno. Il file passa ai container tutte le variabili che i servizi leggono da .env. environment-variables.md le elenca tutte. Vedi sync.md per comprendere la sincronizzazione e come il client la raggiunge.

Crea il primo account

Nessuno può registrarsi autonomamente. Un account si crea aprendo un invito indirizzato a uno specifico indirizzo email. Generi il primo invito per te stesso sul server usando ADMIN_TOKEN. Esegui questo comando in ~/openplate con il tuo indirizzo. Sulla porta 3001 è dove compose.core.yml e compose.full.yml pubblicano il server centrale. Il file compose dedicato del server centrale in apps/core usa invece la porta 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 risposta è una singola riga di JSON. La parte importante si presenta così:

json
"emailed":false,"link":"https://openplate.example.com/join#server=https%3A%2F%2Fsync.example.com&invite=si_..."
  • Nessuna email configurata (il valore predefinito): "emailed": false. Nessun messaggio è stato inviato. Copia il link e aprilo tu stesso.
  • Email configurata (vedi Posta): "emailed": true. Lo stesso link è in arrivo a quell'indirizzo tramite email.

Apri il link in un browser su una pagina protetta, scegli una password e l'account esiste. Un telefono o un secondo dispositivo può accedere solo dopo aver configurato HTTPS, perché il tunnel ssh e localhost servono un solo computer. Il link funziona una volta sola e scade dopo sette giorni. "role":"admin" rende questo primo account un amministratore. Da questo momento in poi, inviti le persone dall'app stessa in /admin. Su un'istanza priva di email, la pagina mostra ciascun nuovo link. Ometti role per un membro ordinario. Posta spiega entrambi i metodi: passare ciascun link a mano, oppure lasciare che sia il server centrale a inviarlo via email.

Su un'istanza gestita, in cui il server centrale paga per le scansioni di tutti, aggiungi "dailyAiLimit":200 al corpo della richiesta per assegnare all'account 200 richieste IA al giorno. Il valore predefinito è 0. Un'istanza gestita richiede altre quattro righe in .env. Le scansioni non partono senza un modello, perché l'app non sceglie un modello a tue spese:

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

Sostituisci vendor/model-name con il modello del tuo provider, scritto esattamente come lo scrive il provider. Al posto di AI_ADVERTISED_MODEL puoi impostare AI_TIERS_FILE=bundled, e il server centrale prenderà il modello dal suo file dei livelli, ai-tiers.json. Vedi configuration.md.

Quando qualcuno dimentica la password

Con l'email configurata, Password dimenticata nell'app invia un link di ripristino, e il diario torna disponibile dopo il reset. Senza email, l'app non può inviare nulla, e la pagina di recupero dice all'utente di rivolgersi all'amministratore. Il link lo crei tu:

  • Nell'app: Amministrazione, sotto Persone, apri la persona e scegli Invia un link di ripristino. Senza email, la pagina mostra il link. Condividilo come faresti con una password.
  • Sul server: trova il id dell'account, poi richiedi un link corrispondente. La porta è ancora la 3001, come nei compose file delle topologie.
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 seconda chiamata risponde con {"emailed":false,"link":"https://openplate.example.com/reset#server=...&token=sr_..."}. Un link di ripristino funziona una volta sola e scade dopo un'ora.

Posta

Il server centrale può inviare messaggi di invito e di reimpostazione della password. Non è obbligato a farlo. Un'istanza familiare funziona perfettamente senza alcuna configurazione di posta, ed è la via più semplice.

Nessuna posta

Lascia non configurata ogni impostazione di posta. Il server centrale non invia alcun messaggio. Mostra invece a te ciascun link, e tu lo trasmetti come faresti per condividere una password.

  • Un invito: apri Amministrazione su /admin e scegli Invita qualcuno. Inserisci l'indirizzo e scegli Invia l'invito. La pagina mostra Invito pronto per quell'indirizzo e mostra il link. Scegli Copia il link e invialo alla persona, ad esempio in un messaggio privato. Chiunque abbia il link può creare l'account.
  • Una password dimenticata: sotto Persone, apri la scheda della persona e scegli Invia un link di ripristino. La pagina mostra il link. Trasmettilo nello stesso modo. Funziona una volta sola, entro un'ora.
  • Un invito smarrito: sotto Inviti, scegli Invia di nuovo accanto all'indirizzo. In questo modo crei un nuovo link e annulli quello precedente. La pagina mostra il nuovo link da copiare. Scegli Torna all'elenco per tornare agli inviti aperti.

Se il link usa un indirizzo diverso da quello del tuo browser, compare un avviso sotto di esso. Imposta PUBLIC_APP_URL e PUBLIC_SYNC_URL con gli indirizzi usati dalla tua famiglia, poi genera nuovamente il link. La pagina ti avvisa anche se il link apre la pagina ma indirizza l'app a un server centrale su localhost o a un semplice indirizzo http:// non raggiungibile da altri dispositivi. Imposta PUBLIC_SYNC_URL con l'indirizzo https:// usato dalla tua famiglia, poi genera nuovamente il link.

OPEN_SIGNUP=true permette a sconosciuti di richiedere un account. Il server centrale rifiuta di avviarsi con questa impostazione quando non ci sono email configurate. Un'istanza familiare la lascia disattivata, e rimane disattivata finché non la imposti. Registrazione con Turnstile illustra l'impostazione e il relativo captcha.

SMTP

Qualsiasi account di posta standard può inviare i messaggi tramite SMTP. Aggiungi queste righe a .env:

  • SMTP_HOST: il nome del server, senza schema e senza porta.
  • SMTP_PORT: 587 se lo lasci vuoto.
  • SMTP_USER e SMTP_PASSWORD: i dati di accesso. Impostali entrambi, oppure lasciali entrambi vuoti per un server che non richiede autenticazione.
  • SMTP_FROM: l'indirizzo del mittente, come semplice indirizzo o Name <address>.
  • MAIL_OPERATOR_EMAIL: il tuo indirizzo personale. Riceve la tua copia di una cancellazione o di un recesso. Entrambi i metodi di trasporto lo richiedono.

La porta determina la modalità di cifratura. La porta 465 usa TLS fin dall'inizio. Qualsiasi altra porta deve passare a STARTTLS, e il servizio non invia messaggi a server che non lo supportano. Il testo in chiaro è consentito solo se SMTP_HOST è un indirizzo di loopback come localhost, per un catcher locale come Mailpit (vedi Testare con Mailpit). Il servizio verifica sempre i certificati.

Un account Gmail richiede una password per le app. Google ne genera una solo per gli account con la verifica in due passaggi attivata. Le impostazioni SMTP di Google specifica smtp.gmail.com e la porta 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 richiede credenziali SMTP create per SES, che differiscono dalla tua chiave di accesso AWS, e un indirizzo del mittente verificato. L'host indica la tua regione AWS. Controlla elenco degli endpoint per maggiori dettagli. Finché il tuo account rimane nella sandbox di SES, SES recapita i messaggi solo agli indirizzi dei destinatari verificati.

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

Ricrea il server centrale con docker compose -f <your file> up -d dopo ogni modifica a .env.

Un'API di posta HTTP

Funziona altrettanto bene un servizio di posta con API HTTP. Imposta MAIL_API_URL, MAIL_API_KEY e MAIL_API_FROM, tutti e tre, più MAIL_OPERATOR_EMAIL. Il server centrale invia ogni messaggio come richiesta JSON POST a MAIL_API_URL, con MAIL_API_KEY come token Bearer, usando il formato previsto dall'API di Resend. Resend è un servizio compatibile. Su Resend, MAIL_API_URL è https://api.resend.com/emails.

Configura un solo metodo di trasporto. Se imposti sia SMTP sia l'API di posta, il server centrale rifiuta di avviarsi.

La posta richiede gli indirizzi pubblici

Ogni messaggio contiene un link, e tale link deve potersi aprire sul telefono del lettore. Quando configuri SMTP o un'API di posta, imposta PUBLIC_APP_URL e PUBLIC_SYNC_URL con gli indirizzi https:// usati dalla tua famiglia. Se una delle due impostazioni usa un semplice http:// o un indirizzo di loopback come localhost, il server centrale rifiuta di avviarsi. Il suo log indica ciascun valore da correggere, con un messaggio che inizia così:

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

Il messaggio di log fa riferimento a questi valori come CLIENT_BASE_URL e SERVER_PUBLIC_URL. Si tratta dei nomi interni letti dal server centrale, e i file compose li mappano da PUBLIC_APP_URL e PUBLIC_SYNC_URL. Visualizza il messaggio con docker compose -f <your file> logs core.

Verifica che la posta funzioni

Invia un invito a un tuo secondo indirizzo da /admin. La pagina dovrebbe mostrare Invito inviato a quell'indirizzo, e il messaggio dovrebbe arrivare nella tua casella di posta. Se la pagina mostra Invito pronto per e stampa un link, la consegna non è riuscita. Il link resta valido. Controlla il log del server centrale alla riga Mail send failed per scoprire la ragione. Quando hai finito i test, seleziona Revoca per l'invito di prova in Inviti.

Testare con Mailpit

Mailpit intercetta ogni messaggio e lo mostra su una pagina web, permettendo di testare la posta senza un account email. Eseguilo come sidecar che condivide la rete del container core. Il server centrale lo raggiunge quindi all'indirizzo localhost, dove è consentito il testo in chiaro. Salva questo file accanto al tuo file compose come 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'

Aggiungi queste righe a .env. Mailpit riceve i messaggi sulla porta 1025:

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

Avvia entrambi i file insieme, poi invia un invito e leggilo su http://localhost:8025 sul server:

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

La regola in La posta richiede gli indirizzi pubblici è sempre valida. Imposta prima PUBLIC_APP_URL e PUBLIC_SYNC_URL con indirizzi https://, altrimenti il server centrale non si avvia.

Un container Mailpit separato raggiunto tramite nome del servizio, come SMTP_HOST=mailpit, non funziona. Il server centrale invia testo in chiaro solo a questa macchina. Un nome di servizio conta come un altro host. Il servizio richiede STARTTLS, Mailpit non lo offre, e ogni messaggio fallisce con Mail send failed nel log. La rete condivisa colloca Mailpit sulla stessa macchina.

Al termine dei test, rimuovi le quattro righe da .env. Quindi elimina Mailpit con docker compose -f compose.core.yml up -d --remove-orphans.

Un relay con un'autorità di certificazione privata

Il server centrale controlla il certificato di ogni server di posta. Un relay all'interno di una rete aziendale può usare un certificato firmato da un'autorità di certificazione privata. Node.js non considera attendibile tale autorità per impostazione predefinita. Fornisci al server centrale il certificato di tale autorità sotto forma di file PEM. Monta il file nel container e imposta NODE_EXTRA_CA_CERTS con il suo percorso. Per esempio, in un compose.ca.yml accanto al tuo file 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 legge il file una volta sola, all'avvio. Il certificato viene aggiunto a quelli già ritenuti attendibili da Node.js, così i server di posta pubblici continuano a funzionare.

L'app con runtime di inferenza in Self-hosting

docker/topologies/compose.inference.yml esegue l'app insieme a openplate-inference. Le foto del piatto vengono lette sul tuo hardware, e ogni visitatore riceve l'opzione con un tocco "questo openplate fornisce la propria IA". Leggi prima la sezione sull'hardware in topologies.md. Il piccolo modello lite richiede circa 1,6 GB di RAM e da pochi secondi a un minuto per piatto su 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 semplice funziona per le scansioni, HTTPS richiede HTTPS per l'intera catena. Su semplice http://<the server's address>, una foto del piatto raggiunge il container di inferenza, ma l'installazione dell'app non funziona. Quando l'app gira su https://, anche l'indirizzo di inferenza deve essere https://. Altrimenti, il browser blocca la chiamata dalla pagina sicura. Vedi HTTPS.

PUBLIC_INFERENCE_URL deve essere un indirizzo che un browser può aprire, perché la foto va dal telefono direttamente al container di inferenza. http://inference:8300/v1, il nome che i container usano tra loro, lì non funziona. Mantieni /v1 alla fine.

Il primo avvio scarica circa 2 GiB di pesi (1,96 GiB) in un volume con nome, operazione che nei nostri test ha richiesto sei o sette minuti, e poi carica il modello. Il log mostra ogni passaggio. L'app è subito attiva; l'IA a tocco singolo funziona quando questo comando risponde 200:

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

La chiave si trova nella pagina caricata da ogni browser, quindi chiunque possa aprire l'app può leggerla. Va bene su una rete domestica o su una tailnet, ma non su un'istanza esposta a Internet. In configuration.md trovi la regola completa. Il container di inferenza usa tutti i core della CPU tranne due; imposta LLAMA_THREADS in .env per modificare questo comportamento.

L'app con sincronizzazione e inferenza in Self-hosting

docker/topologies/compose.full.yml esegue quattro container: l'app, il server centrale, Postgres e l'inferenza in Self-hosting. Leggi prima L'app con il tuo server centrale e L'app con runtime di inferenza in Self-hosting. Questa sezione illustra solo le modifiche necessarie quando tutti i componenti girano insieme.

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 il primo account nello stesso modo descritto in sopra. Al primo avvio vengono scaricati anche i pesi del modello, circa 2 GiB. Il log di caricamento del modello e il controllo di disponibilità funzionano come in L'app con runtime di inferenza in Self-hosting. Per usare dispositivi reali invece di fare una prova, metti tutti e tre gli indirizzi sotto HTTPS. Vedi HTTPS.

Senza Docker

L'app è un unico programma Node.js. Funziona direttamente da un checkout del codice. Ecco come eseguirla su Ubuntu 24.04 con systemd che la mantiene attiva tra disconnessioni e riavvii.

Node.js 24 o più recente. Ubuntu 24.04 fornisce la versione 18 di nodejs, che è troppo vecchia. Installa la 24 da 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

Funziona anche nvm, ma installa Node nella tua cartella home. L'unità systemd qui sotto deve quindi usare quel percorso (command -v node lo mostra a schermo).

pnpm, la versione richiesta dal repository. corepack è incluso con Node 24. Scarica la versione esatta di pnpm definita nel campo packageManager del file package.json dell'app, ma solo all'interno di apps/app. Fuori da quella cartella, pnpm usa il valore predefinito scelto da corepack. Esegui ogni comando pnpm dentro apps/app. Al primo avvio, conferma la richiesta di download.

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

Le impostazioni. Il server legge .env dalla cartella in cui viene eseguito. In produzione rifiuta di avviarsi senza APP_URL:

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

Imposta APP_URL sull'indirizzo aperto dagli utenti, e TRUST_PROXY=1 non appena hai un reverse proxy davanti. Tutte le altre variabili di l'app vanno nello stesso file. HOST=127.0.0.1 fa sì che il server resti in ascolto solo su questa macchina, il che è ideale dietro un proxy sullo stesso host.

Un'unità systemd, così l'app parte all'avvio del sistema e resta in esecuzione dopo la disconnessione. I campi $USER e $HOME qui sotto vengono completati al momento di incollare:

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 mostra il log. Per aggiornare, scarica le modifiche con pull, compila di nuovo e riavvia:

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

La sezione HTTPS si applica qui senza variazioni: punta il reverse proxy sulla porta 3000.

Primo avvio

  1. Apri l'app e segui la breve procedura iniziale. Nessuna registrazione, nessun accesso: chiunque apra l'app su un dispositivo è l'utente di quel dispositivo.
  2. Vai su Impostazioni → IA e collega un provider di IA con la tua chiave API (OpenRouter, Mistral, il tuo endpoint compatibile con OpenAI oppure Anthropic). Consulta configuration.md per la procedura con un clic di OpenRouter e per offrire invece un endpoint fornito dall'istanza.
  3. Fai subito un backup: Impostazioni → Dati e backup → Scarica tutto (JSON). Il tuo diario risiede nello storage di questo browser, quindi un backup è l'unica copia che sopravvive alla cancellazione dei dati del sito o al passaggio a un nuovo dispositivo.
  4. Se più persone scansionano su questa istanza, richiedi una chiave gratuita per il database degli alimenti su lowcarbcheck.org/developers, aggiungila a .env come FOOD_DB_API_KEY=... ed esegui nuovamente docker compose -f <your file> up -d. Senza una chiave, tutti gli utenti dell'istanza condividono una singola quota anonima ridotta. Vedi configuration.md. Con una chiave puoi anche impostare FOOD_DB_BACKFILL=true, che invia a LowCarbCheck come proposte gli alimenti salvati da una risposta dell'IA. Vedi configuration.md.

HTTPS

I browser limitano diverse funzionalità a un contesto sicuro. Un contesto sicuro è una pagina servita tramite https://, oppure da localhost sulla macchina locale. openplate richiede un contesto sicuro per diverse funzioni:

  • Account, accesso e sincronizzazione. L'accesso, la registrazione e l'apertura di un link di invito o ripristino derivano le chiavi tramite la Web Crypto API (crypto.subtle). I browser disabilitano questa API su una semplice pagina http://. Su http://192.168.1.20:3000, queste schermate non funzionano. Anche la condivisione e la console di ricerca falliscono, perché usano la stessa API.
  • Connetti con OpenRouter, l'accesso rapido a OpenRouter, per lo stesso motivo. Incollare manualmente una chiave funziona ovunque.
  • Installazione dell'app e uso offline (il service worker).

Su semplice HTTP da un altro dispositivo, il diario, l'inserimento manuale, i backup e le foto del piatto funzionano ancora. Il pulsante della foto passa il controllo alla fotocamera del telefono tramite un selettore di file, che non richiede una pagina sicura. La guida rapida funziona senza modifiche sul server stesso, poiché localhost è considerato sicuro.

I tuoi dispositivi hanno bisogno di un indirizzo sicuro. Puoi configurarlo in quattro modi: un tunnel ssh per un test rapido, Caddy con un nome di dominio, Caddy su una rete locale senza nome di dominio, oppure Tailscale.

Un test rapido da un computer: un tunnel ssh

Questo è un test per un computer, non una configurazione definitiva. Il tunnel serve solo il computer che lo esegue, e solo mentre il comando è in esecuzione. Un telefono o un secondo dispositivo non possono accedere tramite di esso. Per quelli, configura HTTPS con Caddy qui sotto.

Per testare account e sincronizzazione prima di configurare un certificato, inoltra le due porte al tuo computer. Lascia non impostati PUBLIC_APP_URL e PUBLIC_SYNC_URL affinché mantengano entrambi i valori predefiniti localhost. Lascia non impostata la posta anche in questo caso. Con la posta configurata, il server centrale rifiuta di avviarsi se gli indirizzi dei link contengono localhost. Senza posta si avvia, e copi ciascun link manualmente. Esegui questo comando sul tuo computer, non sul server:

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

Mentre il comando è in esecuzione, apri http://localhost:3000 nel browser. Questa conta come pagina sicura, quindi l'accesso funziona. Anche un link di invito dal server (http://localhost:3000/join#...) si apre lì. Aggiungi -L 8300:localhost:8300 per il container di inferenza.

Un nome di dominio: Caddy

Caddy richiede e rinnova automaticamente un certificato Let's Encrypt. Richiede un nome di dominio puntato verso il tuo server. Richiede anche che le porte 80 e 443 siano raggiungibili da Internet per la verifica del certificato.

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

Poi, imposta APP_URL=https://openplate.example.com in .env. Imposta TRUST_PROXY=1, che è il valore predefinito di produzione. Ricrea il servizio app con docker compose -f compose.yml up -d. Un semplice docker compose restart non rilegge .env. Riavvia solo il container esistente, quindi i nuovi valori non vengono mai caricati.

Per la sincronizzazione, assegna al server centrale un proprio nome di dominio. Imposta entrambi gli URL pubblici invece di 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

Modifica la riga ports: in '127.0.0.1:3000:3000', e usa '127.0.0.1:3001:3000' per la sincronizzazione. Applica la modifica con docker compose -f <your file> up -d. Un reverse proxy non annulla la pubblicazione delle porte dei container. Se lo lasci come '3000:3000', l'app continua a servire HTTP semplice sulla porta 3000 attraverso la tua rete locale accanto all'indirizzo HTTPS.

Podman ricrea il servizio nello stesso modo con podman compose -f compose.yml up -d. Nota un dettaglio di rootless prima di saltare il reverse proxy: un container Podman rootless non può associarsi a una porta host inferiore a 1024 senza configurazione aggiuntiva. Pubblicare direttamente sulla porta 80 o 443 richiede prima sudo sysctl net.ipv4.ip_unprivileged_port_start=80. Vedi podman.md.

Senza nome di dominio, solo rete locale: Caddy con certificato locale

Anche senza un nome di dominio, Caddy può gestire HTTPS sulla tua rete locale. Crea una propria autorità di certificazione e la usa per firmare un certificato per l'indirizzo del server. Ogni telefono e computer che apre openplate deve accettare tale autorità una volta sola. Dopodiché, i telefoni della famiglia ottengono una pagina sicura. L'accesso, la sincronizzazione e l'installazione dell'app funzionano tramite https://.

Assegna un indirizzo fisso al server. Nel router, riserva l'indirizzo attuale del server, per esempio 192.168.1.20, in modo che non cambi mai. Il certificato ed entrambi gli indirizzi pubblici fanno riferimento a questo indirizzo.

Installa Caddy e indirizzalo verso l'indirizzo scelto. Su Ubuntu, sudo apt install caddy installa Caddy come servizio. Sostituisci /etc/caddy/Caddyfile con questo testo, usando l'indirizzo del tuo server. tls internal indica a Caddy di firmare il certificato autonomamente:

# /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

Imposta gli stessi indirizzi in .env, poi ricrea i container 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

Per la sola app, imposta invece APP_URL=https://192.168.1.20. Ometti il secondo blocco dal Caddyfile. Come nella ricetta sopra, cambia le righe ports: in '127.0.0.1:3000:3000' e '127.0.0.1:3001:3000', così che solo Caddy possa raggiungere i container.

Copia il certificato radice di Caddy dal server. La versione di Caddy su Ubuntu lo conserva in /var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt. Il certificato è pubblico. La sua chiave privata si trova nella stessa cartella e non deve mai lasciare il server, quindi copia solo 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

Sul tuo computer, scaricalo con scp you@192.168.1.20:openplate-root.crt .. Invialo poi a ciascun telefono, ad esempio come allegato email a te stesso, o tramite AirDrop.

Aggiungilo ai certificati attendibili su ogni dispositivo, una sola volta. Questo è il passaggio che fa funzionare i telefoni della famiglia su https://.

  • iPhone e iPad: apri il file e autorizza il download. In Impostazioni, tocca Profilo scaricato in alto, o cercalo sotto Generali > VPN e gestione dispositivi, e installalo. Attiva poi l'attendibilità totale per il certificato in Impostazioni > Generali > Info > Impostazioni attendibilità certificati. Senza quest'ultimo passaggio, il browser rifiuta ancora la pagina.
  • Android: salva il file sul telefono. Apri Impostazioni > Sicurezza > Crittografia e credenziali > Installa un certificato > Certificato CA, accetta l'avviso e seleziona il file. Sui telefoni più recenti il percorso inizia da Sicurezza e privacy > Altre impostazioni di sicurezza, e i nomi differiscono leggermente tra i vari produttori. Se il telefono non ha un blocco schermo, Android chiede di impostarne uno.
  • Un computer: aggiungilo ai certificati di sistema. Alcuni browser gestiscono un proprio elenco e richiedono di aggiungerlo anche lì.

Apri https://192.168.1.20 su un telefono. La pagina dovrebbe caricarsi senza avvisi, e l'accesso dovrebbe funzionare. L'indirizzo funziona solo sulla tua rete domestica. Conserva la cartella dei dati di Caddy nei tuoi backup. Una nuova installazione di Caddy genera una nuova autorità, e ogni dispositivo dovrà quindi autorizzare quella nuova.

Qualsiasi altro reverse proxy

nginx, Traefik o un altro proxy possono sostituire Caddy. Non ne abbiamo testato nessuno, quindi questa è una lista di controllo, non una guida passo passo. Il proxy deve svolgere tutte queste operazioni:

  • Due indirizzi https://. L'app e il server centrale ne ricevono ciascuno uno proprio.
  • Gli stessi indirizzi in .env. Imposta PUBLIC_APP_URL e PUBLIC_SYNC_URL esattamente su quegli indirizzi. Per la sola app, si tratta di APP_URL.
  • TRUST_PROXY=1, o il numero di proxy nella catena.
  • Host e X-Forwarded-Proto raggiungono l'app. Trasmetti l'intestazione Host del browser senza modificarla, oppure imposta X-Forwarded-Host su di essa. Imposta X-Forwarded-Proto su https. Il controllo CSRF dell'app ricava da questi dati l'indirizzo della pagina stessa e lo confronta con l'intestazione Origin del browser. Se non sono corretti, l'invio dei moduli fallisce.
  • X-Forwarded-For raggiunge entrambi i servizi. I rispettivi limiti per indirizzo lo leggono.
  • Le porte del container restano su 127.0.0.1. Mantieni '127.0.0.1:3000:3000' e '127.0.0.1:3001:3000', in modo che nulla li raggiunga aggirando il proxy.

Senza nome di dominio: Tailscale Serve

Tailscale fornisce a ogni macchina della tua tailnet un indirizzo HTTPS in ts.net. Non servono nomi di dominio, porte aperte né la gestione manuale dei certificati. Tailscale Serve posiziona quell'indirizzo davanti a una porta su questa macchina. openplate con sincronizzazione necessita di due indirizzi. Servi due porte sotto lo stesso nome di macchina: l'app sulla 443 e il server centrale sulla 8443.

Prima di iniziare:

  • Attiva MagicDNS e i certificati HTTPS per la tua tailnet nella pagina DNS della console di amministrazione di Tailscale. Guida HTTPS di Tailscale contiene i passaggi. Il nome della macchina compare in un registro pubblico dei certificati, quindi scegline uno che non riveli dati privati.
  • Ogni membro della famiglia esegue Tailscale su ogni dispositivo che apre openplate. Ciascuna persona deve far parte della tua tailnet, oppure avere questa macchina condivisa con sé.
  • Mantieni le porte del container su 127.0.0.1: '127.0.0.1:3000:3000' per l'app e '127.0.0.1:3001:3000' per la sincronizzazione. Tailscale Serve le raggiunge su questa macchina. Nient'altro deve avervi accesso.

Poi servi entrambe le porte. --bg le mantiene in esecuzione in background, e Tailscale le serve di nuovo dopo un riavvio:

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

Tailscale rilascia e rinnova il certificato. Imposta entrambi gli indirizzi in .env, usando i nomi della tua macchina e della tua 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 è un reverse proxy, quindi TRUST_PROXY resta a 1. Ricrea lo stack con docker compose -f <your file> up -d. Se esegui l'app da sola, servi solo la porta 3000 e imposta APP_URL sul primo indirizzo.

Non abbiamo provato questo percorso dall'inizio alla fine. Il Riferimento di tailscale serve documenta i flag qui sopra, e La documentazione di Tailscale descrive tailscale serve. La ricetta Caddy sopra è quella che abbiamo verificato.

Headscale, un server di controllo Tailscale self-hosted, non distribuisce certificati HTTPS. Una tailnet Headscale richiede invece la ricetta Caddy con un dominio.

Backup

Sul server dell'app non c'è nulla di cui fare il backup. Non contiene alcun database e non scrive alcuno stato: un container dell'app distrutto non perde nulla.

L'esportazione JSON su ciascun dispositivo è il backup che conta davvero: Impostazioni → Dati e backup → Scarica tutto (JSON). Quel file è la copia che sopravvive alla cancellazione dei dati del browser o a un telefono guasto. L'app mostra un banner di promemoria quando un dispositivo contiene dati che non hai mai esportato, o che non esporti da un po'.

Mantieni l'esportazione privata quanto il diario. Contiene ogni voce in chiaro, oltre alla chiave privata dell'identità di condivisione di questo dispositivo e alla radice da cui deriva lo pseudonimo di ricerca. Chiunque abbia il file può aprire un diario condiviso con questa persona e può ricollegare a lei i suoi contributi allo studio. Due elementi ne restano esclusi: la chiave del provider IA e le foto del piatto, che non lasciano mai il dispositivo.

Se esegui anche il server centrale, vale la pena programmare un dump del suo Postgres, insieme a SERVER_SECRET, che è inutile senza il database e viceversa:

Strumento per i container
docker compose -f compose.core.yml exec postgres \
  pg_dump -U openplate openplate_sync > sync-backup.sql

Aggiornamento

Strumento per i container
docker compose -f compose.yml pull
docker compose -f compose.yml up -d

Usa lo stesso file -f usato per il deployment. Se hai avviato una topologia da docker/topologies/, indica invece quel file, ad esempio docker compose -f compose.core.yml pull. Se esegui solo docker compose pull accanto a compose.core.yml, il comando fallisce con no configuration file provided: not found.

I file compose usano il tag latest, che corrisponde alla versione più recente, quindi pull ti porta a quella. Per scegliere quando aggiornare, fissa una versione nella riga image:, per esempio ghcr.io/lowcarbcheck/openplate:0.54.0. Cambia il numero quando vuoi passare alla versione successiva. Il server centrale e il servizio di inferenza hanno numeri di versione propri, quindi fissa ciascuna immagine alla propria. Il tag main segue ogni modifica sul ramo main. Serve per i test, non per un server usato dalla tua famiglia.

Non c'è nulla da migrare: il container dell'app non mantiene alcuno stato, quindi una nuova immagine si limita a sostituire quella vecchia. Se usi l'intero stack, il server centrale applica le proprie migrazioni all'avvio.

Aggiornamento da un'immagine precedente alla versione 0.1.x (una sola volta)

Le immagini più vecchie eseguivano un sistema di account e un proprio Postgres. Entrambi non esistono più. L'aggiornamento elimina la tabella users e tutto ciò che ne dipende: account, sessioni, token di verifica e di ripristino. Nessun dato che hai registrato viene toccato: i dati del tracker erano già stati spostati sul dispositivo. È una modifica irreversibile, quindi:

  1. Fai prima un backup. Esegui un pg_dump del vecchio database dell'app se vuoi poter recuperare le righe degli account, e fai fare a ciascuna persona su ciascun dispositivo un'esportazione JSON da Profilo → I tuoi dati. Quell'esportazione è la copia che contiene il loro diario.
  2. Aggiorna. Aggiorna prima la riga image: a ghcr.io/lowcarbcheck/openplate:latest, la versione più recente: le immagini precedenti alla 0.1.x venivano pubblicate sotto ghcr.io/sprqvntrs/openplate, e scaricare senza questa modifica recupera semplicemente quella vecchia. La migrazione viene poi eseguita all'avvio del container. Successivamente non c'è alcuna pagina di login: ogni dispositivo che ha già dei dati li conserva e smette semplicemente di chiederti chi sei.
  3. Pulisci il tuo .env. Le variabili per il segreto di sessione, la chiave di cifratura, il blocco delle registrazioni e il superadmin preconfigurato non esistono più, così come DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME e le variabili di configurazione del pool. Nessun componente le legge. Lasciarle impostate non crea problemi, ma sono codice morto.
  4. Elimina il vecchio volume quando il risultato ti soddisfa. Le versioni precedenti non fissavano il nome del progetto Compose, quindi il prefisso derivava dalla directory in cui venivano eseguite (verifica prima il nome effettivo): docker volume ls | grep pg-data, poi docker compose down && docker volume rm <that name>.

Se erano stati effettuati gli accessi a due account nello stesso profilo del browser, tieni presente che l'archivio del dispositivo è sempre stato limitato al singolo dispositivo: i loro dati condividevano già un unico archivio e restano lì. Usare profili del browser separati rimane il modo per tenere distinti i diari di due persone sullo stesso dispositivo.

Modifica questa pagina su GitHub