Laravel 11 with MySQL and queues, end to end
Build a small feedback app in Laravel 11 or newer: a form saves submissions to MySQL, a queued job processes each one in the background, and the scheduler prunes old rows. Then deploy the web app, a queue worker and the scheduler on Deployxa.
What you will build
- A
Submissionmodel and migration, stored in managed MySQL. - A
ProcessSubmissionjob dispatched on thedatabasequeue (no Redis needed). - A web service, a WORKER service running
queue:work, and a Scheduled Task runningschedule:run.
You need PHP 8.2+, Composer, a Deployxa account, and optionally the CLI (npm install -g @deployxa/cli).
1. Create the project
$ composer create-project laravel/laravel deployxa-feedback $ cd deployxa-feedback $ php artisan make:model Submission -m $ php artisan make:job ProcessSubmission
A fresh Laravel 11 app uses SQLite locally and already ships migrations for the jobs, cache and sessions tables, with QUEUE_CONNECTION=database. That is all the queue needs.
2. Model and migration
// database/migrations/xxxx_xx_xx_create_submissions_table.php
public function up(): void
{
Schema::create('submissions', function (Blueprint $table) {
$table->id();
$table->string('email');
$table->text('message');
$table->unsignedInteger('word_count')->nullable();
$table->timestamp('processed_at')->nullable();
$table->timestamps();
});
}// app/Models/Submission.php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class Submission extends Model
{
use Prunable;
protected $fillable = ['email', 'message', 'word_count', 'processed_at'];
protected function casts(): array
{
return ['processed_at' => 'datetime'];
}
// Removed by "php artisan model:prune"
public function prunable(): Builder
{
return static::where('created_at', '<', now()->subDays(30));
}
}3. The queued job
// app/Jobs/ProcessSubmission.php
namespace App\Jobs;
use App\Models\Submission;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
class ProcessSubmission implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public function __construct(public Submission $submission) {}
public function handle(): void
{
// Real apps would call an API, send mail, resize images...
$this->submission->update([
'word_count' => str_word_count($this->submission->message),
'processed_at' => now(),
]);
Log::info('Processed submission', ['id' => $this->submission->id]);
}
}4. Routes and view
// routes/web.php
use App\Jobs\ProcessSubmission;
use App\Models\Submission;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/', fn () => view('feedback', [
'submissions' => Submission::latest()->take(20)->get(),
]));
Route::post('/feedback', function (Request $request) {
$data = $request->validate([
'email' => ['required', 'email'],
'message' => ['required', 'string', 'max:2000'],
]);
ProcessSubmission::dispatch(Submission::create($data));
return back()->with('status', 'Thanks! We are processing your feedback.');
});{{-- resources/views/feedback.blade.php --}}
<!doctype html>
<html>
<head><meta charset="utf-8"><title>Feedback</title></head>
<body style="font-family: system-ui; max-width: 40rem; margin: 3rem auto">
<h1>Send feedback</h1>
@if (session('status')) <p>{{ session('status') }}</p> @endif
<form method="POST" action="/feedback">
@csrf
<input name="email" type="email" placeholder="you@example.com" required>
<textarea name="message" rows="4" required></textarea>
<button>Send</button>
</form>
<h2>Recent</h2>
<ul>
@foreach ($submissions as $s)
<li>{{ $s->email }}: {{ $s->processed_at ? $s->word_count.' words' : 'queued' }}</li>
@endforeach
</ul>
</body>
</html>/up in bootstrap/app.php (health: '/up'). It returns 200 once the app boots, which makes it a handy URL for uptime checks.5. Schedule the pruning
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('model:prune')->daily();6. Run it locally
$ php artisan migrate $ php artisan serve # terminal 1 $ php artisan queue:work # terminal 2: submissions flip from "queued" to a word count
7. Prepare for production
Read the injected database URL
When you attach MySQL, Deployxa injects DATABASE_URL. Laravel reads DB_URL, so fall back to the injected value in the mysql connection:
// config/database.php ('connections' => 'mysql')
'url' => env('DB_URL', env('DATABASE_URL')),If config/database.php does not exist yet (Laravel 11 hides unchanged config files), publish it first with php artisan config:publish database.
Trust the proxy
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: '*');
})Generate the key
$ php artisan key:generate --show base64:9n4c…
8. Deploy the web service
- 1Import
Push to GitHub, open
/dashboard/projects/newand choose the repository. - 2Configure
The Laravel preset is selected from
artisan: buildcomposer install --no-dev --optimize-autoloader, web rootpublic. Set Database to MySQL. - 3Environment
Paste these into the Bulk / .env tab:
envAPP_NAME=Feedback APP_ENV=production APP_DEBUG=false APP_KEY=base64:your-generated-key APP_URL=https://your-app-url DB_CONNECTION=mysql QUEUE_CONNECTION=database SESSION_DRIVER=database CACHE_STORE=database LOG_CHANNEL=stderr
- 4Deploy
The release runs
php artisan migrate --force, creatingsubmissions,jobs,cacheandsessions. If your project does not show migrations in the deploy log, add a Scheduled Task or worker start step that runs it (see step 9).
From the CLI the same deploy is:
$ rm -rf vendor node_modules && mv .env ../feedback.env # keep local files out of the upload $ deployxa deploy $ deployxa env set APP_KEY "base64:your-generated-key" $ deployxa env set APP_ENV production $ deployxa env set APP_DEBUG false $ deployxa logs --live
9. Add the queue worker
Submitting the form now stores a row and a job, but nothing processes the job yet. On the project's Environment Canvas, add a service:
Give the worker the same variables as the web service (at least APP_KEY, DB_CONNECTION and the database connection). --max-time=3600 makes the worker exit hourly so it is restarted with fresh memory; each deploy restarts it with the new code.
sh -c 'php artisan migrate --force && php artisan queue:work --tries=3 --max-time=3600' as its start command.10. Run the scheduler
Open the Scheduled Tasks tab and add a task named laravel-scheduler with schedule * * * * *, strategy Container Exec and command php artisan schedule:run. Laravel then decides which scheduled commands are due, so model:prune runs once a day.
11. Verify and add a domain
Submit the form on your Deployxa URL. Within a few seconds the entry changes from "queued" to a word count, and the worker's logs show Processed submission. Connect feedback.example.com under Dashboard → Domains (or deployxa domains add feedback.example.com), then update APP_URL and redeploy.
Troubleshooting
See Deploy Laravel for the full list of settings and errors.