Dockerize a Laravel + MySQL App and Deploy to DigitalOcean App Platform
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:
# ── 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:
[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:
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:
node_modules
.git
.env
.env.*
storage/logs/*
vendor
tests
*.test.js
Build and test locally:
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 DigitalOcean → Databases → Create 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:
-- 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 Platform → Create 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:
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:
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:
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 downis 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:
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:
- 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
- Want full server control instead of App Platform? → — the Spring Boot Droplet guide covers the raw VPS path: systemd services, Nginx reverse proxy, and Certbot HTTPS. The same approach works for any server-side framework.
- Comparing VPS vs. serverless for a JS stack? → — the Next.js breakdown gives a cost model and decision matrix that applies equally well when you're choosing App Platform vs. a Droplet for a PHP app.
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 |