- Distroless base image - No shell, bash, or package managers
- Non-root user - Runs as
nonrootuser (UID 65532) - Disabled dangerous PHP functions -
exec,shell_exec,system, etc. - Minimal attack surface - Only essential files and libraries
- Read-only filesystem ready - All writes go to
/tmpor volumes - Security headers - Proper session configuration
- OPcache enabled with preloading for maximum performance
- Multi-stage build - Optimized layer caching
- Optimized Composer autoloader - Classmap authoritative
- Alpine-based build stages - Smaller intermediate layers
- Production-only dependencies - No dev packages
- Minimal final image - Distroless base
- No unnecessary tools - No shell, debugging tools, or utilities
- Optimized PHP extensions - Only required extensions included
- Smart .dockerignore - Excludes tests, docs, and dev files
Before building, ensure your Laravel application has:
composer.jsonandcomposer.lock- Proper
.env.example(.envshould NOT be in the image) - Built frontend assets or
package.jsonfor build process
# Build the image
docker build -t my-laravel-app:latest .
# Build with BuildKit (recommended for best caching)
DOCKER_BUILDKIT=1 docker build -t my-laravel-app:latest .
# Build with specific platform
docker build --platform linux/amd64 -t my-laravel-app:latest .docker run -d \
--name laravel-fpm \
-p 9000:9000 \
-v $(pwd)/storage:/var/www/html/storage \
-e APP_KEY=your-app-key \
-e DB_HOST=mysql \
-e DB_DATABASE=laravel \
-e DB_USERNAME=laravel \
-e DB_PASSWORD=secret \
my-laravel-app:latestservices:
app:
image: my-laravel-app:latest
container_name: laravel-fpm
restart: unless-stopped
volumes:
- ./storage:/var/www/html/storage
environment:
APP_NAME: Laravel
APP_ENV: production
APP_KEY: ${APP_KEY}
APP_DEBUG: false
APP_URL: https://yourdomain.com
DB_CONNECTION: mysql
DB_HOST: mysql
DB_PORT: 3306
DB_DATABASE: laravel
DB_USERNAME: laravel
DB_PASSWORD: ${DB_PASSWORD}
CACHE_DRIVER: redis
SESSION_DRIVER: redis
QUEUE_CONNECTION: redis
REDIS_HOST: redis
REDIS_PASSWORD: null
REDIS_PORT: 6379
networks:
- laravel
depends_on:
- mysql
- redis
healthcheck:
test: ["CMD-SHELL", "/usr/local/sbin/php-fpm -t"]
interval: 30s
timeout: 3s
retries: 3
nginx:
image: nginx:alpine
container_name: laravel-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./storage/app/public:/var/www/html/storage/app/public:ro
- ./public:/var/www/html/public:ro
networks:
- laravel
depends_on:
- app
mysql:
image: mysql:8.0
container_name: laravel-mysql
restart: unless-stopped
environment:
MYSQL_DATABASE: laravel
MYSQL_USER: laravel
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
networks:
- laravel
redis:
image: redis:7-alpine
container_name: laravel-redis
restart: unless-stopped
networks:
- laravel
networks:
laravel:
driver: bridge
volumes:
mysql_data:apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-fpm
spec:
replicas: 3
selector:
matchLabels:
app: laravel-fpm
template:
metadata:
labels:
app: laravel-fpm
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: php-fpm
image: my-laravel-app:latest
imagePullPolicy: Always
ports:
- containerPort: 9000
protocol: TCP
env:
- name: APP_KEY
valueFrom:
secretKeyRef:
name: laravel-secrets
key: app-key
- name: DB_HOST
value: "mysql-service"
- name: DB_DATABASE
value: "laravel"
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: laravel-secrets
key: db-username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: laravel-secrets
key: db-password
volumeMounts:
- name: storage
mountPath: /var/www/html/storage
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
exec:
command:
- /usr/local/sbin/php-fpm
- -t
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
tcpSocket:
port: 9000
initialDelaySeconds: 5
periodSeconds: 10
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumes:
- name: storage
persistentVolumeClaim:
claimName: laravel-storage-pvcThe following directories should be mounted as volumes (not included in image):
/var/www/html/storage/app/var/www/html/storage/framework/cache/var/www/html/storage/framework/sessions/var/www/html/storage/framework/views/var/www/html/storage/logs
All configuration should be done via environment variables. Never bake .env into the image.
Required variables:
APP_KEY- Laravel application keyAPP_ENV- Set toproductionAPP_DEBUG- Set tofalse- Database credentials (DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD)
Since there's no shell in the container, you need to exec PHP directly:
# Using docker exec
docker exec laravel-fpm php artisan migrate
# Using kubernetes
kubectl exec -it laravel-fpm-pod -- php artisan config:cacheYou'll need an Nginx container to serve static files and proxy to PHP-FPM:
server {
listen 80;
server_name yourdomain.com;
root /var/www/html/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass laravel-fpm:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}- Always use secrets management (Kubernetes Secrets, Docker Secrets, etc.)
- Enable read-only root filesystem in Kubernetes
- Use network policies to restrict pod communication
- Regularly scan images for vulnerabilities
- Use specific image tags, never
:latestin production
Since there's no shell, debugging requires:
- Check logs:
docker logs laravel-fpm - Use PHP-FPM status page at
/status - Use ping endpoint at
/ping - Monitor error logs via stderr
- Traditional PHP-FPM image: ~400-500MB
- This optimized image: ~150-200MB
- Reduction: ~60-70%
MIT License.