Deployment

Dockerize a Laravel + MySQL App and Deploy to DigitalOcean App Platform

July 26, 202616 min read

Disclosure: Some links in this guide are affiliate links. If you sign up for DigitalOcean through them, ToolsHubKit earns a small commission — at no cost to you.

DigitalOcean App Platform is the managed alternative to running your own VPS. You push a Docker image or connect a Git repo, and the platform handles server provisioning, HTTPS, autoscaling, and zero-downtime deploys. There are no SSH sessions to manage and no Nginx configs to write.

This guide builds a production-ready multi-stage Dockerfile for Laravel, deploys it to App Platform, wires up a Managed MySQL database, and covers the operational details — migrations on deploy, queue workers, and scheduled tasks.

Prerequisites:

  • A working Laravel 11+ application
  • Docker installed locally
  • A DigitalOcean account (get $200 in free credits)
  • Basic Docker familiarity

App Platform vs. a Raw Droplet

App Platform Droplet (VPS)
Server management None You manage patching, reboots
HTTPS Automatic Certbot + manual renewal
Scaling Slider in UI New Droplets + load balancer
Deploy workflow Push to GitHub or registry SSH + restart service
Cost predictability Per-container billing Fixed monthly
Root access No Yes

App Platform is the right call when you want to ship fast and avoid ops overhead. Use a Droplet when you need full control, specific kernel configs, or co-locating services that don't containerize well.


1. Write a Production Dockerfile

Laravel's production image needs PHP-FPM to run PHP and Nginx to serve HTTP traffic. We use a multi-stage build so the final image doesn't carry Composer, development dependencies, or build tooling.

Create Dockerfile in your Laravel project root:

dockerfile
# ── Stage 1: Composer dependency install ──────────────────────────────────────
FROM composer:2.7 AS vendor

WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
    --no-dev \
    --no-scripts \
    --no-autoloader \
    --ignore-platform-reqs \
    --prefer-dist

COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative

# ── Stage 2: Frontend assets (if you use Vite/Mix) ───────────────────────────
FROM node:22-alpine AS assets

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# ── Stage 3: Production image ─────────────────────────────────────────────────
FROM php:8.3-fpm-alpine

# Install required PHP extensions
RUN apk add --no-cache \
        nginx \
        supervisor \
        bash \
    && docker-php-ext-install \
        pdo_mysql \
        opcache \
        pcntl \
    && docker-php-ext-enable opcache

# PHP-FPM config
COPY docker/php-fpm.conf /usr/local/etc/php-fpm.d/www.conf
# Nginx config
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/nginx-app.conf /etc/nginx/http.d/default.conf
# Supervisor manages both PHP-FPM and Nginx
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf

WORKDIR /var/www/html

# Copy app from previous stages
COPY --from=vendor /app /var/www/html
COPY --from=assets /app/public/build /var/www/html/public/build

RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache \
    && chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache

EXPOSE 8080
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

Docker support files

docker/supervisord.conf — runs PHP-FPM and Nginx under one process:

ini
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0

[program:php-fpm]
command=/usr/local/sbin/php-fpm --nodaemonize
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

docker/nginx-app.conf — forwards requests to PHP-FPM:

nginx
server {
    listen 8080 default_server;
    root /var/www/html/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

.dockerignore — keeps the build context lean:

code
node_modules
.git
.env
.env.*
storage/logs/*
vendor
tests
*.test.js

Build and test locally:

bash
docker build -t my-laravel-app .
docker run -p 8080:8080 \
  -e APP_KEY=base64:... \
  -e DB_HOST=127.0.0.1 \
  my-laravel-app

2. Provision a Managed MySQL Database

Sign in to DigitalOceanDatabasesCreate Database Cluster.

Settings:

  • Engine: MySQL 8
  • Region: Match your App Platform region for lowest latency
  • Plan: Basic 1 GB (sufficient for most small apps) — you can resize later without downtime

Once provisioned, navigate to the cluster's Connection Details tab. You'll see:

  • Host (something like db-mysql-xxx.db.ondigitalocean.com)
  • Port: 25060 (TLS-only port)
  • Username: doadmin
  • Password: auto-generated
  • CA certificate (download this)

DigitalOcean Managed MySQL only accepts TLS connections on port 25060. Laravel handles this transparently — just set the host and port correctly.

Create a dedicated database and restricted user:

sql
-- Run in the DigitalOcean database UI or via mysql CLI
CREATE DATABASE laraveldb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'laraveluser'@'%' IDENTIFIED BY 'strong-password-here';
GRANT ALL PRIVILEGES ON laraveldb.* TO 'laraveluser'@'%';
FLUSH PRIVILEGES;

3. Deploy to App Platform

In the DigitalOcean console, go to App PlatformCreate App.

Option A: Deploy from a GitHub repo

Connect your GitHub account, select your repo and branch. App Platform detects the Dockerfile automatically.

Option B: Deploy from a Docker registry

Push your image first:

bash
docker tag my-laravel-app registry.digitalocean.com/YOUR_REGISTRY/laravel-app:latest
docker push registry.digitalocean.com/YOUR_REGISTRY/laravel-app:latest

Then create the app pointing at the registry image.

Configure environment variables

In the App Platform UI under Environment Variables, add:

code
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:YOUR_KEY_HERE
APP_URL=https://your-app-domain.com

DB_CONNECTION=mysql
DB_HOST=db-mysql-xxx.db.ondigitalocean.com
DB_PORT=25060
DB_DATABASE=laraveldb
DB_USERNAME=laraveluser
DB_PASSWORD=strong-password-here
DB_SSL_CA=/etc/ssl/certs/ca-certificates.crt

CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis

For Redis (sessions, cache, queues), add a Redis cluster from DigitalOcean's managed databases — or use the App Platform's built-in Redis add-on.


4. Migrations on Deploy

App Platform supports run commands that execute before traffic is routed to the new container. Use this for artisan migrate:

In the App Platform UI, under your app's Settings → Components → Run Command, or in your app.yaml:

yaml
name: laravel-app
services:
- name: web
  dockerfile_path: Dockerfile
  http_port: 8080
  run_command: php artisan migrate --force
  envs:
  - key: APP_ENV
    value: production
  # ... other env vars

--force is required in production to skip the "are you sure?" prompt. Run commands execute serially before the new container receives traffic, so migrations always complete before new code is served.

Zero-downtime considerations

  • Additive migrations only: Always add columns before removing old code that reads them, and remove columns only after old code is no longer deployed. This prevents the running instance from failing to read a column that was just dropped.
  • php artisan down is not safe on App Platform — the old container handles traffic until the new one is healthy. Design migrations to work alongside both the old and new application code simultaneously.

5. Queue Workers and the Scheduler

Queue worker component

Add a second App Platform component of type Worker pointing to the same Docker image but with a different run command:

yaml
workers:
- name: queue
  dockerfile_path: Dockerfile
  run_command: php artisan queue:work --sleep=3 --tries=3 --max-time=3600
  instance_count: 1
  instance_size_slug: basic-xxs

Worker components don't expose HTTP ports and restart automatically if the queue worker process exits.

Scheduler

Laravel's scheduler needs php artisan schedule:run every minute. On App Platform, add another worker:

yaml
- name: scheduler
  dockerfile_path: Dockerfile
  run_command: php artisan schedule:work

schedule:work (added in Laravel 8) is a blocking loop that runs the scheduler every minute — no cron daemon required.


6. Environment Config and Secrets

Never commit .env files. In production on App Platform:

  • Mark sensitive values (passwords, API keys) as Secret in the App Platform env vars UI — they're encrypted at rest and masked in logs.
  • Reference Managed Database credentials using App Platform's database attachment feature so the connection string is injected automatically without you hardcoding it.

7. Logs, Metrics, and Scaling

Logs: In the App Platform UI, each component has a Runtime Logs tab. Logs stream in real time. For persistent log storage, ship to DigitalOcean Spaces via a Laravel logging channel or use a service like Papertrail.

Metrics: App Platform shows CPU, memory, and HTTP request rate per component.

Scaling: Go to your component → Edit → adjust instance count (horizontal) or instance size (vertical). Horizontal scaling on a web component requires your app to be stateless — sessions in Redis, uploads to Spaces, no local disk writes.


Cost Snapshot

For a typical small Laravel application:

Component Spec Monthly cost
Web worker Basic XXS (512 MB) ~$5
Queue worker Basic XXS ~$5
Managed MySQL 1 GB Basic ~$15
Managed Redis 1 GB Basic ~$15
Total ~$40/mo

Compare that to a raw 4 GB Droplet at $24/mo — App Platform costs more but eliminates all ops overhead. At small scale the extra cost buys time.


Related Guides


Resources

Resource Link
Start with $200 free DigitalOcean credits Claim credits →
Generate a Dockerfile for your app Dockerfile Generator
Build API requests to test your endpoints cURL Generator
DigitalOcean App Platform docs https://docs.digitalocean.com/products/app-platform/
Laravel deployment docs https://laravel.com/docs/deployment