Dockerizing a Laravel Application: A Complete Guide
A complete Dockerfile, docker-compose stack, and Nginx configuration for running a Laravel app in containers, from local dev to production.
Continuous Integration and Continuous Deployment turn "remember to run the tests before merging" from a hopeful habit into something that happens automatically, every single time. GitHub Actions is the easiest way to set this up for a web app already hosted on GitHub.
A workflow lives in .github/workflows/*.yml and defines triggers (when to run) and jobs (what to run):
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- run: composer install --prefer-dist --no-progress
- run: cp .env.example .env
- run: php artisan key:generate
- run: php artisan test
This alone catches a broken PR before a human ever needs to notice — every push and every PR automatically runs the full test suite.
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8
env:
MYSQL_DATABASE: testing
MYSQL_ROOT_PASSWORD: secret
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
GitHub Actions' services block spins up a real MySQL container alongside the test job — your test suite hits an actual database, not a mock, which catches real migration/query bugs that a mocked DB never would.
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /var/www/app
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
needs: test means deployment only runs if the test job passed — a failing test suite physically cannot reach production. Secrets (SSH keys, server credentials) are stored in GitHub's encrypted repository secrets, never in the workflow file itself.
- uses: actions/cache@v4
with:
path: vendor
key: composer-${{ hashFiles('composer.lock') }}
Without caching, every run reinstalls every Composer package from scratch — slow and wasteful when composer.lock hasn't changed since the last run. This single addition often cuts CI time by more than half.
A complete Dockerfile, docker-compose stack, and Nginx configuration for running a Laravel app in containers, from local dev to production.
Structuring a real multi-service Docker Compose stack — app, worker, database with health checks, and environment-specific overrides.