CI/CD for Web Apps with GitHub Actions
Automated testing and deployment with GitHub Actions — running a real database in CI, and deploying automatically on merge to main.
Dockerizing a Laravel app means your entire team (and your production server) runs the exact same PHP version, extensions, and services — no more "works on my machine." Here's a complete, working setup.
FROM php:8.3-fpm
RUN apt-get update && apt-get install -y \
libpq-dev libzip-dev unzip git \
&& docker-php-ext-install pdo pdo_mysql zip
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www
COPY . .
RUN composer install --no-dev --optimize-autoloader
CMD ["php-fpm"]
Multi-stage-copying Composer directly from its own official image (rather than installing it via curl) keeps the Dockerfile shorter and always pulls a known-good build.
services:
app:
build: .
volumes:
- .:/var/www
depends_on:
- db
- redis
nginx:
image: nginx:alpine
ports:
- "8000:80"
volumes:
- .:/var/www
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
db:
image: mysql:8
environment:
MYSQL_DATABASE: laravel
MYSQL_ROOT_PASSWORD: secret
volumes:
- dbdata:/var/lib/mysql
redis:
image: redis:alpine
volumes:
dbdata:
Four services, one command to bring the whole stack up: docker compose up -d.
server {
listen 80;
root /var/www/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
fastcgi_pass app:9000 uses the service name app as the hostname — Docker Compose's internal DNS resolves it automatically, no hardcoded IPs needed.
# .env
DB_HOST=db
REDIS_HOST=redis
Inside the Docker network, containers reach each other by service name, not 127.0.0.1 — a very common first-time mistake is leaving DB_HOST=127.0.0.1 from local development and wondering why the app can't connect to the database.
docker compose exec app php artisan migrate
docker compose exec app php artisan queue:work
Once this is running, the exact same docker-compose.yml (with production-appropriate environment values swapped in) is what deploys to a real server — the dev and production environments are now provably identical.
Automated testing and deployment with GitHub Actions — running a real database in CI, and deploying automatically on merge to main.
Structuring a real multi-service Docker Compose stack — app, worker, database with health checks, and environment-specific overrides.