Building an Offline AI App with Laravel and Ollama
Most AI tutorials start with "get an API key." This one doesn't. By the end, you'll have a Laravel app that answers questions using an AI model running on your own computer. No API key, no monthly bill, and no data leaving your machine.
You need internet once, to download the tools and the model. After that, you can switch off your Wi-Fi and it keeps working.
Why offline AI?
A few real reasons:
- Privacy - Some clients will not let you send their data to a third-party service. A local model solves that.
- Cost - Hosted AI APIs charge per request. A local model is free to run once it's downloaded.
- Reliability - No rate limits, no outages, and it works on a train.
- Learning - You get to see what's actually happening instead of treating AI as a black box.
The trade-off is speed and quality. A small local model is slower and less clever than the big hosted ones. For learning, internal tools, and simple assistants, it's plenty.
What we're building
A single page with a text box. You type a question, click a button, and the answer appears below. Here's the flow:
Browser -> Laravel -> Ollama -> AI model
Browser <- Laravel <- Ollama <- AI model
- LLM (large language model) - the AI brain that reads your text and writes a reply.
- Ollama - a free program that downloads LLMs and runs them on your computer. It also gives you a small web API on localhost, which is how Laravel talks to it.
- localhost - your own computer. Nothing here goes out to the internet.
What you need
- PHP 8.2 or newer
- Composer
- About 4 GB of free disk space and 8 GB of RAM (more RAM makes it faster)
- Ollama (we'll install it next)
Step 1: Install Ollama and get a model
Download Ollama from ollama.com (macOS and Windows), or on Linux run:
curl -fsSL https://ollama.com/install.sh | sh
Now download a small model. We'll use Llama 3.2, which is around 2 GB:
ollama pull llama3.2
Quick test in the terminal:
ollama run llama3.2
Type "Hello" and you should get a reply. Type /bye to exit.
Ollama normally runs in the background after installation. If you get a "connection refused" error later, start it manually with ollama serve.
Let's also check the API that Laravel will use. On macOS or Linux:
curl http://localhost:11434/api/chat -d '{
"model": "llama3.2",
"stream": false,
"messages": [{"role": "user", "content": "Say hello in one short sentence."}]
}'
You'll get JSON back, and the reply lives inside message.content. (On Windows PowerShell, quoting JSON is annoying, so skip this check. The Laravel app will test it for you anyway.)
Step 2: Create the Laravel project
composer create-project laravel/laravel offline-ai
cd offline-ai
We don't need a database for this app, and the default setup works fine as is.
Step 3: Add the settings
Open .env and add these lines at the bottom:
OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=llama3.2
OLLAMA_TIMEOUT=120
The timeout is in seconds. Local models can take a while on the first request because the model has to load into memory.
Now open config/services.php and add this entry inside the returned array, next to the other services:
'ollama' => [
'url' => env('OLLAMA_URL', 'http://localhost:11434'),
'model' => env('OLLAMA_MODEL', 'llama3.2'),
'timeout' => env('OLLAMA_TIMEOUT', 120),
],
Why go through the config file instead of reading .env directly? Laravel can cache config for speed, and env() stops working outside config files when that happens. Reading from config() is the safe habit.
Step 4: Write a service class
A service class is just a normal PHP class that does one job. Ours will talk to Ollama, which keeps that code out of the controller.
Create app/Services/OllamaService.php (make the Services folder if it doesn't exist):
<?php
namespace App\Services;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class OllamaService
{
public function ask(string $question): string
{
$url = rtrim(config('services.ollama.url'), '/') . '/api/chat';
try {
$response = Http::timeout((int) config('services.ollama.timeout'))
->post($url, [
'model' => config('services.ollama.model'),
'stream' => false,
'messages' => [
[
'role' => 'system',
'content' => 'You are a helpful assistant. Keep answers short and clear.',
],
[
'role' => 'user',
'content' => $question,
],
],
]);
} catch (ConnectionException $e) {
throw new RuntimeException('Could not reach Ollama. Is it running?');
}
if ($response->failed()) {
throw new RuntimeException('Ollama returned an error: ' . $response->body());
}
return $response->json('message.content', '');
}
}
What's going on here:
- The
system messagesets the assistant's behavior. Theuser messageis what the person typed. 'stream' => falsetells Ollama to send the whole answer at once instead of word by word. It's simpler for a first project.- If Ollama isn't running, we catch the error and throw a friendly message instead of a scary stack trace.
Step 5: Create the controller
php artisan make:controller ChatController
Replace the contents of app/Http/Controllers/ChatController.php with:
<?php
namespace App\Http\Controllers;
use App\Services\OllamaService;
use Illuminate\Http\Request;
use RuntimeException;
class ChatController extends Controller
{
public function show()
{
return view('chat');
}
public function ask(Request $request, OllamaService $ollama)
{
$data = $request->validate([
'question' => ['required', 'string', 'max:1000'],
]);
try {
$answer = $ollama->ask($data['question']);
} catch (RuntimeException $e) {
return back()->withInput()->withErrors(['question' => $e->getMessage()]);
}
return back()->withInput()->with('answer', $answer);
}
}
Laravel builds OllamaService for you automatically because it's listed as a parameter. That's called dependency injection, and you don't have to set anything up for it.
Step 6: Add the routes
Replace everything in routes/web.php with:
<?php
use App\Http\Controllers\ChatController;
use Illuminate\Support\Facades\Route;
Route::get('/', [ChatController::class, 'show']);
Route::post('/ask', [ChatController::class, 'ask']);
Step 7: Build the page
Create resources/views/chat.blade.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Offline AI Assistant</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 40px auto; padding: 0 16px; }
textarea { width: 100%; height: 100px; padding: 8px; box-sizing: border-box; }
button { margin-top: 8px; padding: 8px 16px; cursor: pointer; }
.answer { margin-top: 24px; padding: 16px; background: #f4f4f5; border-radius: 6px; white-space: pre-wrap; }
.error { color: #b91c1c; margin-top: 8px; }
</style>
</head>
<body>
<h1>Offline AI Assistant</h1>
<form method="POST" action="/ask"
onsubmit="var b = this.querySelector('button'); b.disabled = true; b.innerText = 'Thinking...';">
@csrf
<textarea name="question" placeholder="Ask me something...">{{ old('question') }}</textarea>
@error('question')
<div class="error">{{ $message }}</div>
@enderror
<button type="submit">Ask</button>
</form>
@if (session('answer'))
<div class="answer">{{ session('answer') }}</div>
@endif
</body>
</html>
@csrf adds a hidden security token that Laravel requires on every form. Forget it and you'll get a "419 Page Expired" error. The little onsubmit script just disables the button and changes its label, so you know something is happening while the model thinks.
Step 8: Run it
Make sure Ollama is running, then start Laravel:
php artisan serve
Open http://127.0.0.1:8000 and try this question:
Explain what a Laravel service class is in two sentences.
The first answer may take 10 to 30 seconds while the model loads. After that it's usually much quicker. You should see a short answer appear in the grey box under the form. The exact wording changes every time you ask, so yours won't match anyone else's word for word. That's normal.
The offline test
This is the fun part. Turn off your Wi-Fi or unplug your network cable, then ask another question. It still works, because the model lives on your machine and Laravel is only talking to localhost.
Common problems
- Could not reach Ollama - Ollama isn't running. Start it with
ollama serveand try again. - Error mentioning a model not found - The name in
.envdoesn't match what you downloaded. Runollama listto see your models and copy the exact name. - It's very slow - Small models on older laptops can be sluggish. Close heavy apps, or try a smaller model like
llama3.2:1b(changeOLLAMA_MODEL in .env, and remember to runollama pull llama3.2:1bfirst). - Timeout errors - Raise
OLLAMA_TIMEOUT in .env. On some setups, especially Windows, PHP's ownmax_execution_time(30 seconds by default) can also cut things short. You can raise it in php.ini.
Changes to .env are ignored. Run php artisan config:clear.
Where to go next
Once this works, a few natural upgrades:
- Chat history - Store past messages in the database and send them along with each request so the assistant remembers the conversation.
- Streaming - Show the answer word by word as it's generated, like ChatGPT does.
- Chat with your own documents - This is called RAG. You turn your documents into numbers (embeddings) using a local model such as nomic-embed-text, then search them for relevant pieces to give the model as context. It's a great follow-up project.
- Use it inside a CMS - Draft summaries or tag suggestions for your content without sending anything to an outside service.
Now, You built a working AI feature with a handful of files and no outside accounts. The same pattern (Laravel talks to a local Ollama server over HTTP) scales from this small demo to real internal tools. Start small, get it running, and then improve it one piece at a time.
No more posts to load.
- CIBB - Basic Forum With Codeigniter and Twitter Bootstrap
- Build an AI-Driven personalisation engine in Joomla using User Behaviour data
- Steps to create a Contact Form in Symfony With SwiftMailer
- Building a RAG System in Laravel from Scratch
- Laravel and Prism PHP: The Modern Way to Work with AI Models
- Drupal 7 - Create your custom Hello World module
- Build an AI Code Review Bot with Laravel — Real-World Use Case
- Prototype Javascript validation in Magento forms
- Creating and checking user session using CodeIgniter library
- Symfony Framework - Introduction