Laravel + Vue CI/CD in 2026: Docker, GitHub Actions, and Coolify from Local to Production

If you can build a Laravel + Vue app locally but still panic every time you deploy, this tutorial is for you.
In this guide, you’ll build a complete CI/CD pipeline that takes your code from local machine to production safely. We’ll use:
- Laravel 12 (API + backend logic)
- Vue 3 + Vite (frontend)
- Docker (consistent runtime)
- GitHub Actions (automated test/build/deploy checks)
- Coolify (self-hosted deployment platform)
By the end, you’ll have a workflow where every push is validated, every deployment is repeatable, and every rollback is predictable.
Difficulty
Intermediate — You should know basic Laravel, basic Vue, and basic Git.
What you’ll build
A production-ready pipeline with:
- Local development with Docker Compose
- Multi-stage Docker image for production
- GitHub Actions workflow for linting, testing, and image checks
- Coolify deployment with environment variables and health checks
- Safe release process (migrations, queues, caching)
- Rollback strategy and monitoring checklist
Prerequisites
Before starting, make sure you have:
- A GitHub repository with your Laravel + Vue project
- Docker Desktop or Docker Engine (24+)
- Node.js 20+ (for local frontend tooling)
- A VPS with Docker installed (Hetzner, OVH, DigitalOcean, etc.)
- A Coolify instance already running
- Domain/subdomain configured (optional but recommended)
Recommended stack versions used in this tutorial:
- PHP 8.3
- Laravel 12
- Vue 3
- Vite 6
- PostgreSQL 16
- Redis 7
Step 1 — Structure your Laravel + Vue project for containerized deployment
If your frontend is embedded in Laravel resources (Vite), your root may look like this:
.
├── app/
├── bootstrap/
├── config/
├── database/
├── public/
├── resources/
│ ├── css/
│ └── js/
├── routes/
├── storage/
├── tests/
├── composer.json
├── package.json
└── vite.config.jsAdd these root-level files:
.dockerignoreDockerfiledocker-compose.yml(local dev)docker-compose.prod.yml(optional for parity testing)
Create .dockerignore:
.git
.github
node_modules
vendor
storage/logs
storage/framework/cache
storage/framework/sessions
storage/framework/views
.env
.env.*
.DS_StoreWhy this matters: smaller image context = faster build = cheaper CI.
⚠️ Warning: Never bake
.envfiles into your image. Runtime secrets should always come from environment variables.
Step 2 — Create a multi-stage Dockerfile (PHP-FPM + Nginx + built assets)
A common mistake is shipping a giant dev image to production. We’ll avoid that.
# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS frontend_builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY resources ./resources
COPY vite.config.* ./
COPY public ./public
RUN npm run build
FROM composer:2.8 AS php_vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--optimize-autoloader \
--no-interaction \
--prefer-dist
FROM php:8.3-fpm-alpine AS app
RUN apk add --no-cache \
bash \
icu-dev \
libpng-dev \
libzip-dev \
oniguruma-dev \
postgresql-dev \
nginx \
supervisor \
curl
RUN docker-php-ext-install pdo pdo_pgsql mbstring intl zip opcache
WORKDIR /var/www/html
COPY . .
COPY --from=php_vendor /app/vendor ./vendor
COPY --from=frontend_builder /app/public/build ./public/build
RUN chown -R www-data:www-data storage bootstrap/cache
RUN chmod -R 775 storage bootstrap/cache
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisord.conf
EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]Example docker/supervisord.conf:
[supervisord]
nodaemon=true
[program:php-fpm]
command=php-fpm -F
autostart=true
autorestart=true
[program:nginx]
command=nginx -g "daemon off;"
autostart=true
autorestart=true💡 Tip: Keep app runtime image lean and deterministic. Build assets and vendor dependencies in earlier stages only.
Step 3 — Define local development services with Docker Compose
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: noqta_app
ports:
- "8080:80"
env_file:
- .env
depends_on:
- db
- redis
volumes:
- ./:/var/www/html
db:
image: postgres:16-alpine
container_name: noqta_db
restart: unless-stopped
environment:
POSTGRES_DB: noqta
POSTGRES_USER: noqta
POSTGRES_PASSWORD: secret
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
container_name: noqta_redis
ports:
- "6379:6379"
volumes:
pgdata:Run:
docker compose up -d --build
docker compose exec app php artisan key:generate
docker compose exec app php artisan migrate
docker compose exec app php artisan testIf this doesn’t pass locally, CI won’t save you.
🚀 Need help implementing this in your product stack? Noqta’s API Integration team can build and harden your deployment architecture end-to-end.
Step 4 — Prepare production-safe Laravel configuration
In your .env.example, ensure production toggles are explicit:
APP_ENV=production
APP_DEBUG=false
LOG_CHANNEL=stack
LOG_LEVEL=info
QUEUE_CONNECTION=redis
CACHE_STORE=redis
SESSION_DRIVER=redis
DB_CONNECTION=pgsqlProduction bootstrap script (docker/entrypoint.sh):
#!/usr/bin/env bash
set -e
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Run migrations safely (consider --force with backup strategy)
php artisan migrate --force
exec "$@"Make executable:
chmod +x docker/entrypoint.sh⚠️ Warning: Auto-running migrations on every deploy is convenient, but dangerous for destructive schema changes. Use expand/contract migration patterns for zero-downtime systems.
Step 5 — Add quality gates in GitHub Actions
Create .github/workflows/ci.yml:
name: CI
on:
pull_request:
push:
branches: ["main"]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
- name: Install PHP dependencies
run: composer install --no-interaction --prefer-dist
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install JS dependencies
run: npm ci
- name: Build frontend
run: npm run build
- name: Prepare environment
run: |
cp .env.example .env
php artisan key:generate
- name: Run migrations
env:
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
DB_PORT: 5432
DB_DATABASE: testdb
DB_USERNAME: postgres
DB_PASSWORD: postgres
REDIS_HOST: 127.0.0.1
run: php artisan migrate --force
- name: Run tests
env:
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
DB_PORT: 5432
DB_DATABASE: testdb
DB_USERNAME: postgres
DB_PASSWORD: postgres
REDIS_HOST: 127.0.0.1
run: php artisan test --parallelOptional but recommended extra jobs:
- PHPStan / Larastan
- Pint (Laravel code style)
- ESLint / TypeScript checks
- Trivy container scan
This is where teams gain confidence: no human approval needed for obvious failures.
Step 6 — Build a deployable container image in CI
Create .github/workflows/build-image.yml:
name: Build and Push Image
on:
workflow_run:
workflows: ["CI"]
types:
- completed
jobs:
build:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}/app
tags: |
type=sha
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}Now every successful main commit produces a versioned image.
Step 7 — Deploy with Coolify
In Coolify:
- Create New Resource → Application
- Source: GitHub or container registry (
ghcr.io/...) - Set build/deploy mode: Dockerfile or prebuilt image
- Configure env vars (APP_KEY, DB creds, Redis, mail, etc.)
- Add domain + SSL
- Define health check endpoint (
/upin Laravel)
Create a simple route for health:
Route::get('/up', fn () => response()->json(['status' => 'ok', 'time' => now()]));In Coolify deployment settings:
- Health check path:
/up - Timeout: 10s
- Retries: 5
- Auto deploy on push: enabled (for main branch)
💡 Tip: Use branch-based environments:
mainfor production,stagingfor pre-prod. Never test risky migrations in production first.
Step 8 — Production hardening checklist
Before calling this “done”, verify:
App security
APP_DEBUG=false- strict CORS rules
- rate limiting for auth endpoints
- secure cookies + HTTPS only
Infrastructure
- daily DB backups + retention policy
- server firewall (only 80/443 exposed)
- non-root Docker operation where possible
Runtime
- queue worker process supervised
- failed jobs alerting configured
- centralized logs (Loki, ELK, or at least persisted files)
Performance
- OPcache enabled
- Redis cache working
- Nginx gzip/brotli enabled
- Laravel config/routes/views cached
Step 9 — Rollback strategy that actually works
Most teams discuss rollback. Few test it.
Use immutable image tags (commit SHA). Keep at least last 5 stable images.
Rollback workflow:
- Incident detected (errors/latency)
- Pause new deploys
- Repoint Coolify to previous stable SHA image
- Redeploy
- Validate
/upand core user flows - Open incident report with root cause and corrective action
For database rollbacks:
- avoid destructive migrations in same release
- use feature flags for risky changes
- keep backward-compatible schema for one release cycle
⚠️ Warning: Code rollback is easy; schema rollback is not. Design migrations around reversibility.
💡 Ready to move from tutorial to a production-grade delivery process? Noqta’s Web Development team can build, audit, and operate your full Laravel/Vue platform.
Step 10 — Observability minimum for small teams
You don’t need a giant SRE budget, but you need visibility.
At minimum collect:
- Request latency (p95)
- Error rate (% of 5xx)
- Queue backlog size
- DB CPU/memory and connection saturation
- Deploy frequency + failure rate
A practical starter setup:
- Uptime monitor (Uptime Kuma)
- Log aggregation (Loki + Grafana)
- Basic APM (OpenTelemetry-compatible agent if available)
- Alert to Telegram/Slack for critical failures
For Laravel logs structure:
Log::channel('stack')->info('checkout_completed', [
'order_id' => $order->id,
'user_id' => $order->user_id,
'total' => $order->total,
]);Structured logs make incident response much faster.
Step 11 — Common deployment mistakes (and fixes)
1) Building assets on the production server
Problem: slow deploys, high memory spikes.
Fix: build in CI and ship prebuilt assets.
2) No health checks
Problem: failed deploy still receives traffic.
Fix: enforce /up health gate before routing.
3) Running queue workers inside web container without supervision
Problem: background jobs silently stop.
Fix: separate workers or supervise process explicitly.
4) Shared mutable state in containers
Problem: unpredictable behavior after scaling.
Fix: move state to DB/Redis/object storage.
5) No staging environment
Problem: production becomes your testing environment.
Fix: mirror prod using staging with masked real-world data.
Step 12 — Suggested team workflow
A clean workflow for small product teams:
- Feature branch per task
- Pull request with CI checks required
- Squash merge to
main - CI tests + image build
- Coolify auto-deploy to staging
- Manual smoke test checklist
- Promote same image tag to production
This reduces “works on my machine” and improves traceability.
Final summary
You now have a practical deployment system for Laravel + Vue in 2026:
- Docker gives runtime consistency
- GitHub Actions gives automated confidence
- Coolify gives self-hosted deployment control
- Health checks + rollback strategy reduce downtime risk
- Observability turns panic into process
If you implement only one thing today, start with CI quality gates. If you implement two, add immutable image deploys. That combination alone removes most deployment chaos.
Quick recap
- Local containerized environment
- Multi-stage production image
- Automated CI tests and build
- Coolify deployment setup
- Security and performance hardening
- Rollback and monitoring basics
Build once. Deploy safely. Iterate with confidence.
Discuss Your Project with Us
We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.
Let's find the best solutions for your needs.
Related Articles

Building a Complete CI/CD Pipeline with GitHub Actions for Next.js
Learn how to build a production-grade CI/CD pipeline using GitHub Actions for your Next.js app — with automated linting, unit tests, E2E tests, and deployment to Vercel.

Building a Custom Code Interpreter for LLM Agents
Learn how to create a custom code interpreter for Large Language Model (LLM) agents, enabling dynamic tool calling and isolated code execution for enhanced flexibility and security.

Improve GitLab Communication with Webhooks
Learn how to use GitLab webhooks to centralize communication and improve response times. Includes code examples in Laravel and Next.js.