Mi sono divertito un sacco a realizzare l’immagine del nerd che gongola con la birra “localhost”, volevo evitare la solita copertina con i loghetti dei software. E forse questo paragrafo rovinerà l’indicizzazione di questa pagina, ma non importa, in questo magazine non siamo ossessionati dal bisogno di popolarità.
Tra soluzioni cloud e API proprietarie, spicca una strada alternativa e sempre più interessante, il self-hosting. Questo articolo pensato per sviluppatori alle prime armi ma curiosi, ti guiderà passo-passo nella configurazione locale di LM Studio, nell’esposizione delle sue API, e infine nell’accesso al modello GPT-OSS-20B tramite un’applicazione PHP ospitata in Laragon. Un progetto pratico e replicabile, perfetto per chi vuole sperimentare, imparare o iniziare a costruire soluzioni aziendali autonome e sostenibili. Si presuppone che il lettore possieda conoscenze di base in ambito di reti e programmazione.
LM Studio: la base operativa
LM Studio è un’interfaccia desktop per l’uso locale di modelli di linguaggio open-source, come LLaMA, Mistral e GPT-OSS. Uno dei suoi punti di forza è la possibilità di esporre localmente un’API compatibile con OpenAI, rendendo l’integrazione con app esistenti straordinariamente semplice.
1. Installazione di LM Studio
- Scarica LM Studio da lmstudio.ai
- Installa come un normale programma desktop (disponibile per Windows, macOS e Linux)
- Avvia LM Studio e scegli un modello, ad esempio GPT-OSS-20B, ottimo compromesso tra potenza e leggerezza.
2. Esporre le API
Premetto che nel momento in cui sto scrivendo la documentazione ufficiale non è proprio una meraviglia, soprattutto per gli utenti alle prime armi. Vediamo di colmare questo gap con uno screenshot.

- Una volta caricato il tuo modello preferito, nella colonna di sinistra seleziona Developer.
- In alto, accanto a Status: Running clicca su Settings e copia la configurazione che vedi nello screenshot per esporre le API di LM Studio SOLO in localhost. Se vuoi renderle disponibili a tutta la tua rete locale dovrai abilitare anche Serve on Local Network.
- Clicca sul modello corrente, nella colonna di destra potrai leggere The local server is reachable at this address http://127.0.0.1:1234 quindi localhost (127.0.0.1) alla porta 1234. Se la porta è già occupata da un’altra applicazione cambiala. Se attivi Serve on Local Network il punto di accesso sarà del tipo http://192.168.1.20:1234
Con LM Studio abbiamo finito, passiamo al web server in locale, il mio preferito è Laragon.
Laragon: web server facile
1. Scarica Laragon
- Vai alla pagina Download e prendi l’ultima Full (consigliata), con Apache/Nginx, MySQL, PHP, Node, Python, ecc.
2. Installa
- Avvia l’installer → “Next, Next, Next…”.
- Percorso consigliato:
C:\laragon. - (Opzionale) Spunta Run Laragon when Windows starts per averlo pronto ad ogni avvio.
3. Avvia i servizi
- Apri Laragon e clicca Start All (Apache/Nginx, MySQL, ecc.).
- Con Quick Start puoi creare un progetto (es. WordPress) in pochi minuti.
4. Dove mettere i siti (Document Root) + URL “belli”
- Metti i progetti in
C:\laragon\www\. - Con Auto Virtual Hosts ottieni URL del tipo
http://miosito.test(basta mettere la cartellamiositoinwwwe fare Reload). - Il pattern
{name}.testè configurabile da Menu → Preferences.
5. HTTPS locale in 1 click
- Attiva Menu → Apache → SSL → Enabled: il tuo sito sarà raggiungibile anche in https://miosito.test.
6. Database & phpMyAdmin
- Laragon può aggiungere phpMyAdmin dal menu Tools → Quick add → phpmyadmin.
- Con PHP 8.4+ usa phpMyAdmin 6 (voce phpmyadmin6.0snapshot).
- La pagina di download indica anche i due collegamenti: phpMyAdmin 5 e phpMyAdmin 6 nel menu MySQL.
7. Cambiare versioni di PHP/Apache
- Aggiungi versioni con Tools → Quick add → [PHP/Apache/Nginx Version].
- Cambia la versione di PHP da Menu → PHP → Version → [scelta].
8. Crea il primo progetto “al volo”
- Crea la cartella
myappin cui mettete i file php inC:\laragon\www\myappe generahttp://myapp.test.
9. Scriviamo il codice della nostra applicazione php
Prima programmiamo index.php per la UI
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<title>Chat LLM locale (LM Studio) — Demo PHP</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body{font-family:system-ui,Segoe UI,Arial,sans-serif;max-width:760px;margin:32px auto;padding:0 16px}
textarea{width:100%;min-height:140px;padding:10px}
pre{background:#f6f6f6;padding:12px;border-radius:8px;white-space:pre-wrap}
.row{margin:12px 0}
.muted{color:#666}
button{padding:10px 16px;border:0;border-radius:8px;background:black;color:white;cursor:pointer}
button:hover{opacity:.9}
.card{border:1px solid #eee;border-radius:12px;padding:16px;margin-top:16px}
.error{color:#b00020}
</style>
</head>
<body>
<h1>Chat con LLM locale (LM Studio)</h1>
<p class="muted">Endpoint locale chiamato da PHP: <code>http://localhost:1234/v1/chat/completions</code></p>
<form id="chat-form" class="card">
<div class="row">
<label for="prompt"><strong>Prompt</strong></label><br>
<textarea id="prompt" name="prompt" placeholder="Scrivi la tua richiesta..."></textarea>
</div>
<div class="row">
<label for="temperature">Temperature (0–1)</label><br>
<input id="temperature" name="temperature" type="number" min="0" max="1" step="0.1" value="0.7">
</div>
<div class="row">
<label for="max_tokens">Max tokens (opzionale)</label><br>
<input id="max_tokens" name="max_tokens" type="number" placeholder="es. 512">
</div>
<div class="row">
<button type="submit">Invia</button>
</div>
<p class="muted">Assicurati in LM Studio: <em>Settings → Developer → Enable Local Inference Server</em> (porta predefinita 1234).</p>
</form>
<div id="output" class="card" style="display:none;">
<h3>Risposta del modello</h3>
<pre id="answer"></pre>
</div>
<div id="error" class="card" style="display:none;">
<h3>Errore</h3>
<pre class="error" id="errText"></pre>
</div>
<script>
const form = document.getElementById('chat-form');
const outBox = document.getElementById('output');
const ans = document.getElementById('answer');
const errBox = document.getElementById('error');
const errText = document.getElementById('errText');
form.addEventListener('submit', async (e) => {
e.preventDefault();
outBox.style.display = 'none';
errBox.style.display = 'none';
const prompt = document.getElementById('prompt').value.trim();
const temperature = document.getElementById('temperature').value;
const max_tokens = document.getElementById('max_tokens').value;
if (!prompt) {
errText.textContent = 'Prompt mancante.';
errBox.style.display = 'block';
return;
}
ans.textContent = 'Sto pensando...';
outBox.style.display = 'block';
try {
const res = await fetch('chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
body: new URLSearchParams({ prompt, temperature, max_tokens })
});
const data = await res.json();
if (!res.ok || data.error) {
errText.textContent = (data.error ? JSON.stringify(data.error, null, 2) : `HTTP ${res.status}`);
errBox.style.display = 'block';
outBox.style.display = 'none';
return;
}
ans.textContent = data.content || '(nessuna risposta)';
outBox.style.display = 'block';
} catch (err) {
errText.textContent = String(err);
errBox.style.display = 'block';
outBox.style.display = 'none';
}
});
</script>
</body>
</html>Poi codifichiamo chat.php per la connessioni alle API
<?php
// chat.php — chiama l'API locale di LM Studio (compatibile OpenAI)
header('Content-Type: application/json; charset=utf-8');
$BASE_URL = 'http://localhost:1234/v1'; // cambia se usi altra porta/host
$MODEL = 'openai/gpt-oss-20b'; // deve corrispondere al modello caricato in LM Studio
$SYSTEM = 'Sei un assistente conciso e utile.';
$prompt = trim($_POST['prompt'] ?? '');
if ($prompt === '') {
http_response_code(400);
echo json_encode(['error' => 'Prompt mancante']);
exit;
}
// Opzioni opzionali dal form
$temperature = isset($_POST['temperature']) && $_POST['temperature'] !== '' ? floatval($_POST['temperature']) : 0.7;
$max_tokens = isset($_POST['max_tokens']) && $_POST['max_tokens'] !== '' ? intval($_POST['max_tokens']) : null;
$payload = [
'model' => $MODEL,
'messages' => [
['role' => 'system', 'content' => $SYSTEM],
['role' => 'user', 'content' => $prompt],
],
'temperature' => $temperature,
];
if ($max_tokens !== null) $payload['max_tokens'] = $max_tokens;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $BASE_URL . '/chat/completions',
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
// Se hai configurato un token sul server locale, sblocca la riga sotto:
// 'Authorization: Bearer not-needed',
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 60,
]);
$response = curl_exec($ch);
$err = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false) {
http_response_code(500);
echo json_encode(['error' => ['type' => 'curl_error', 'message' => $err]]);
exit;
}
$data = json_decode($response, true);
// Se LM Studio risponde con errore o HTTP non 2xx
if ($httpCode < 200 || $httpCode >= 300) {
http_response_code($httpCode);
echo json_encode(['error' => $data ?: $response], JSON_UNESCAPED_UNICODE);
exit;
}
// Estrai il testo in stile OpenAI Chat Completions
$content = $data['choices'][0]['message']['content'] ?? null;
echo json_encode([
'content' => $content,
'model' => $MODEL,
'usage' => $data['usage'] ?? null,
], JSON_UNESCAPED_UNICODE);Puntiamo il browser a http://myapp.test

Con questo setup iniziale hai già in mano una base solida, semplice da mantenere e pronta a crescere. Agganciando Bootstrap per dare struttura all’interfaccia e qualche animazione leggera durante l’attesa della risposta dell’LLM, l’esperienza diventerebbe subito più fluida e piacevole.
Ma il vero divertimento inizia adesso, potremmo introdurre un avatar virtuale che ci assista, presenti i contenuti e ci guidi tra le funzioni, trasformando una semplice chat in un’interazione più naturale e memorabile. Da qui si aprono strade praticamente infinite, streaming token-by-token, prompt library condivisa, logging e analisi delle conversazioni, autenticazione, fino a integrazioni con servizi esterni.
In sintesi, abbiamo costruito le fondamenta giuste. Il prossimo passo è rifinire la UX e sperimentare con piccoli miglioramenti iterativi che, sommati, faranno la differenza tra un prototipo funzionale e uno strumento capace di generare valore reale.
Se l’articolo ti è piaciuto restiamo in contatto su linkedin a https://www.linkedin.com/in/andreatonin/
Laragon #PHP #SviluppoLocale #LMStudio #LLM #API #Apache #MySQL #SviluppoWeb #DevTools
Nerd per passione e per professione da oltre 30 anni, lavoro nel mondo dell’innovazione tecnologica come CTO e consulente, progettando ecosistemi software complessi e scalabili. Parallelamente mi dedico alla formazione informatica, condividendo esperienze e buone pratiche maturate sul campo.
Scopri di più sulla mia attività di consulenza su lucedigitale.com Mi trovi anche su LinkedIn



















