DeployxaDocs Portal
CLI v1.0.4
Tutorial: Laravel 11 + MySQL & Queues
Tutorial · Laravel

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 Submission model and migration, stored in managed MySQL.
  • A ProcessSubmission job dispatched on the database queue (no Redis needed).
  • A web service, a WORKER service running queue:work, and a Scheduled Task running schedule:run.

You need PHP 8.2+, Composer, a Deployxa account, and optionally the CLI (npm install -g @deployxa/cli).

1. Create the project

shell
$ 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

php
// 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();
    });
}
php
// 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

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

php
// 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.');
});
blade
{{-- 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>
Note: Laravel 11 registers a health route at /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

php
// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('model:prune')->daily();

6. Run it locally

shell
$ 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:

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

php
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->trustProxies(at: '*');
})

Generate the key

shell
$ php artisan key:generate --show
base64:9n4c…

8. Deploy the web service

  1. 1
    Import

    Push to GitHub, open /dashboard/projects/new and choose the repository.

  2. 2
    Configure

    The Laravel preset is selected from artisan: build composer install --no-dev --optimize-autoloader, web root public. Set Database to MySQL.

  3. 3
    Environment

    Paste these into the Bulk / .env tab:

    env
    APP_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
  4. 4
    Deploy

    The release runs php artisan migrate --force, creating submissions, jobs, cache and sessions. 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:

shell
$ 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:

FieldValue
Service Nameworker
Service TypeWORKER (Background)
Source Directory/
Start Commandphp artisan queue:work --tries=3 --max-time=3600
Depends Onthe web 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.

Tip: If you would rather migrate from the worker than the web release, use 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

SymptomFix
Entries stay "queued"The worker is not running or cannot reach the database. Check the worker service logs and its variables.
419 Page Expired on submitSessions are not persisting: run migrations so the sessions table exists, and keep APP_KEY the same on every service.
SQLSTATE[HY000] [2002] Connection refusedThe url fallback to DATABASE_URL is missing or MySQL is not attached.
Base table or view not found: jobsMigrations did not run; check the deploy log for migrate --force.
Failed jobs pile upInspect them with php artisan queue:failed (via a Container Exec task) and retry with queue:retry all.

See Deploy Laravel for the full list of settings and errors.