Back to Blog
Tutorial11 min read2026-07-01

How to Deploy Laravel with a Managed MySQL Database

Deploying Laravel in production means more than uploading code — you need PHP-FPM, migrations, a queue worker, and a real MySQL database. This guide containerizes a Laravel app and wires it to managed MySQL.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Laravel is one of the most productive backend frameworks around, but the gap between php artisan serve and a production deployment is wide. You need a proper web server (Nginx + PHP-FPM or FrankenPHP), database migrations that run at the right time, a queue worker for jobs, and a managed database so you're not babysitting MySQL on a VM.

This tutorial walks through containerizing a Laravel app and connecting it to a managed MySQL database.

Architecture

We'll run three logical pieces:

  • Web container — PHP-FPM + Nginx (or a single FrankenPHP binary) serving HTTP.
  • Queue workerphp artisan queue:work for background jobs.
  • Managed MySQL — provisioned separately, connected via DATABASE_URL / env vars.

Step 1: Containerize Laravel

The cleanest modern option is [FrankenPHP](https://frankenphp.dev), which bundles a production web server and PHP into one binary. Here's a minimal Dockerfile:

FROM dunglas/frankenphp:1-php8.3 AS base

WORKDIR /app

# System deps for common Laravel extensions
RUN install-php-extensions pdo_mysql gd intl zip opcache pcntl

# Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# Install PHP deps first for better layer caching
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist

# Copy app and finish autoload
COPY . .
RUN composer dump-autoload --optimize \
  && php artisan config:cache \
  && php artisan route:cache \
  && php artisan view:cache

EXPOSE 8080
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]

A companion Caddyfile for FrankenPHP:

{
  frankenphp
  auto_https off
}
:8080 {
  root * /app/public
  php_server
}

We disable internal HTTPS because TLS is terminated at the platform's ingress.

Step 2: Provision managed MySQL

Laravel works with MySQL 5.7 and 8.x. On [PandaStack](https://dashboard.pandastack.io), create a managed MySQL database (provisioned via KubeBlocks on GKE) and pick the version your app targets. The platform exposes connection details as environment variables and injects DATABASE_URL automatically when the database is linked to your app.

Laravel can consume a single URL or discrete variables. The discrete form maps cleanly:

DB_CONNECTION=mysql
DB_HOST=<managed-host>
DB_PORT=3306
DB_DATABASE=<db-name>
DB_USERNAME=<user>
DB_PASSWORD=<password>

If you prefer the URL form, Laravel 9+ supports DB_URL:

DB_URL=${DATABASE_URL}

Step 3: Set the rest of your environment

Laravel needs an app key and sensible production settings:

APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:...        # php artisan key:generate --show
APP_URL=https://yourapp.com
SESSION_DRIVER=database
QUEUE_CONNECTION=database
CACHE_STORE=database

Using database drivers for sessions/queue/cache keeps you stateless without needing Redis on day one. When you scale up, switch these to a managed Redis instance for performance.

Never commit APP_KEY. Generate it once and store it as a secret env var in the dashboard.

Step 4: Run migrations safely

The biggest footgun in Laravel deploys is *when* migrations run. Running them in the Docker build is wrong — the database may be unreachable, and parallel build replicas would race. Run them as a release/deploy step that executes once before the new version takes traffic.

php artisan migrate --force

The --force flag is required in production (it bypasses the interactive confirmation). On PandaStack you set this as a pre-deploy command so it runs once per release.

Step 5: Add a queue worker

Background jobs (emails, image processing, webhooks) should run in a separate process so they don't block web requests. Deploy a second container/app from the same image with a different start command:

php artisan queue:work --tries=3 --max-time=3600 --sleep=3

Key flags:

  • --tries=3 retries failed jobs before marking them failed.
  • --max-time=3600 restarts the worker hourly to avoid memory creep.
  • --sleep=3 polls every 3s when the queue is empty.

For scheduled tasks (app/Console/Kernel.php), deploy a cronjob that runs php artisan schedule:run every minute rather than keeping a long-lived scheduler process.

Step 6: Deploy

  1. 1Push the repo with the Dockerfile to GitHub.
  2. 2Create a container app on PandaStack and connect the repo — it builds the Dockerfile with rootless BuildKit and pushes to the registry.
  3. 3Link the managed MySQL database so DATABASE_URL is injected.
  4. 4Set the migrate command as a pre-deploy hook.
  5. 5Deploy the web app, then the queue worker app from the same image.
  6. 6Add your custom domain — SSL is issued automatically.

Production checklist

ConcernRecommendation
Config cachingconfig:cache + route:cache in the image
OPcacheEnabled (huge throughput win for PHP)
Migrationsmigrate --force as a one-time deploy step
Sessions/cachedatabase driver initially, Redis at scale
Storage uploadsUse object storage, not local disk (containers are ephemeral)
Health checkAdd a /up route (Laravel 11 ships one)

A note on ephemeral filesystems

Containers don't keep local files between deploys or restarts. Any storage/app/public uploads must go to object storage (S3-compatible) via Laravel's filesystem config. Treat the container disk as throwaway.

References

  • [Laravel Deployment docs](https://laravel.com/docs/deployment)
  • [FrankenPHP documentation](https://frankenphp.dev/docs/)
  • [Laravel Queues](https://laravel.com/docs/queues)
  • [Laravel Database configuration](https://laravel.com/docs/database)
  • [MySQL 8.0 Reference Manual](https://dev.mysql.com/doc/refman/8.0/en/)

---

Managed MySQL takes the scariest part of a Laravel deploy off your plate — no patching, scheduled and manual backups, and a connection string injected straight into your app. PandaStack's free tier includes one managed database and enough build minutes to get going; start at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also