You are going to deploy a Laravel application to a VPS:
- PHP-FPM for request handling
- Nginx as web server (static files + PHP proxy)
- MySQL database with persistent storage
- Redis for cache and queues
- Queue worker and scheduler as separate containers
- HTTPS with automatic certificates
This is production-grade Laravel hosting on your own VPS.
What you will have at the end
https://your-domain.comserving your Laravel app- Background queue processing
- Scheduled tasks running via cron
- Database backups ready to configure
Step 1: Prepare your Laravel project
Make sure your .env.example has all required variables:
APP_NAME=MyApp
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.com
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=
REDIS_HOST=redis
REDIS_PORT=6379
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
Step 2: Create the Dockerfile
Create Dockerfile:
FROM php:8.3-fpm-alpine AS base
# Install system dependencies
RUN apk add --no-cache \
git \
curl \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
libzip-dev \
zip \
unzip \
mysql-client \
oniguruma-dev
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip opcache
# Install Redis extension
RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& apk del .build-deps
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
# Production stage
FROM base AS production
# Copy application
COPY . .
# Install dependencies (no dev)
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Set permissions
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
# Optimize Laravel
RUN php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
USER www-data
EXPOSE 9000
CMD ["php-fpm"]
Create nginx.conf:
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php;
charset utf-8;
# Gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
# Static files
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public";
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Step 3: Create docker-compose.yml
services:
traefik:
image: traefik:v2.11
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
- --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
restart: unless-stopped
nginx:
image: nginx:alpine
restart: unless-stopped
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./public:/var/www/html/public:ro
depends_on:
- app
labels:
- traefik.enable=true
- traefik.http.routers.laravel.rule=Host(`${APP_DOMAIN}`)
- traefik.http.routers.laravel.entrypoints=websecure
- traefik.http.routers.laravel.tls=true
- traefik.http.routers.laravel.tls.certresolver=letsencrypt
- traefik.http.services.laravel.loadbalancer.server.port=80
app:
build:
context: .
target: production
restart: unless-stopped
env_file:
- .env
volumes:
- ./storage:/var/www/html/storage
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
queue:
build:
context: .
target: production
restart: unless-stopped
command: php artisan queue:work --sleep=3 --tries=3 --max-time=3600
env_file:
- .env
volumes:
- ./storage:/var/www/html/storage
depends_on:
- app
- redis
scheduler:
build:
context: .
target: production
restart: unless-stopped
command: sh -c "while true; do php artisan schedule:run --verbose; sleep 60; done"
env_file:
- .env
volumes:
- ./storage:/var/www/html/storage
depends_on:
- app
db:
image: mysql:8.0
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
volumes:
letsencrypt:
mysql_data:
redis_data:
Step 4: Set up environment
Create .env on your VPS:
APP_NAME=MyApp
APP_ENV=production
APP_KEY=base64:your-key-here
APP_DEBUG=false
APP_URL=https://your-domain.com
APP_DOMAIN=your-domain.com
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=strong-password-here
DB_ROOT_PASSWORD=root-password-here
REDIS_HOST=redis
REDIS_PORT=6379
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
[email protected]
Generate an app key locally and copy it:
php artisan key:generate --show
Step 5: Deploy
# Create storage directories with correct permissions
mkdir -p storage/framework/{sessions,views,cache}
mkdir -p storage/logs
chmod -R 775 storage bootstrap/cache
# Start the stack
docker compose up -d --build
# Run migrations
docker compose exec app php artisan migrate --force
# Clear caches (first deploy)
docker compose exec app php artisan optimize:clear
docker compose exec app php artisan optimize
Verify everything is running:
docker compose ps
docker compose logs -f app
Step 6: Maintenance commands
Common artisan commands through Docker:
# Run migrations
docker compose exec app php artisan migrate
# Clear all caches
docker compose exec app php artisan optimize:clear
# Rebuild caches
docker compose exec app php artisan optimize
# Tinker
docker compose exec app php artisan tinker
# Queue restart (after code changes)
docker compose exec app php artisan queue:restart
Database backup script (backup.sh):
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR=~/backups/laravel
mkdir -p $BACKUP_DIR
docker compose exec -T db mysqldump -u laravel -p'your-password' laravel | gzip > $BACKUP_DIR/db_$DATE.sql.gz
find $BACKUP_DIR -mtime +7 -delete
echo "Backup completed: $BACKUP_DIR/db_$DATE.sql.gz"
Troubleshooting
Storage permission errors
Laravel needs write access to storage:
# Fix permissions
docker compose exec app chown -R www-data:www-data /var/www/html/storage
docker compose exec app chmod -R 775 /var/www/html/storage
# Or from host
sudo chown -R 82:82 storage # 82 is www-data in Alpine
Queue jobs not processing
Check the queue worker logs:
docker compose logs -f queue
Common issues:
- Redis not connected (check REDIS_HOST)
- Job class not found (run
composer dump-autoload) - Supervisor not restarting after deploys (use
queue:restart)
MySQL connection refused
Wait for the health check to pass:
docker compose ps # Check db health status
# Test connection manually
docker compose exec db mysql -u laravel -p -e "SELECT 1;"
Slow first request after deploy
Laravel caches are cold. Run optimization:
docker compose exec app php artisan optimize
docker compose exec app php artisan view:cache
Internal links (recommended next reads)
- Tutorial: Deploy Docker Compose - understand multi-container setups
- Tutorial: Self-Host Supabase - alternative database
- Comparison: Laravel Forge Alternatives
- ServerCompass: Deploy Laravel in one click
Related in the StoicSoft network
If you regularly stitch together PDF, image, video, or batch-file workflows like the ones above, 1FileTool is the StoicSoft network's purpose-built desktop app — 245+ local-first tools, pay-once, files never leave the device.
From across the StoicSoft network
Hand-curated reads on the same topic from sister sites in the StoicSoft family.

