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

AI Bot
By AI Bot ·

Loading the Text to Speech Audio Player...

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:

  1. Local development with Docker Compose
  2. Multi-stage Docker image for production
  3. GitHub Actions workflow for linting, testing, and image checks
  4. Coolify deployment with environment variables and health checks
  5. Safe release process (migrations, queues, caching)
  6. 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.js

Add these root-level files:

  • .dockerignore
  • Dockerfile
  • docker-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_Store

Why this matters: smaller image context = faster build = cheaper CI.

⚠️ Warning: Never bake .env files 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 test

If 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=pgsql

Production 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 --parallel

Optional 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:

  1. Create New Resource → Application
  2. Source: GitHub or container registry (ghcr.io/...)
  3. Set build/deploy mode: Dockerfile or prebuilt image
  4. Configure env vars (APP_KEY, DB creds, Redis, mail, etc.)
  5. Add domain + SSL
  6. Define health check endpoint (/up in 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: main for production, staging for 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:

  1. Incident detected (errors/latency)
  2. Pause new deploys
  3. Repoint Coolify to previous stable SHA image
  4. Redeploy
  5. Validate /up and core user flows
  6. 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:

  1. Feature branch per task
  2. Pull request with CI checks required
  3. Squash merge to main
  4. CI tests + image build
  5. Coolify auto-deploy to staging
  6. Manual smoke test checklist
  7. 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.


Want to read more tutorials? Check out our latest tutorial on 8 Laravel 11 Basics: Views.

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 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.

15 min read·