Stu Mason
Stu Mason
Guide

Deploying Laravel to Production with Coolify: My Exact Setup

Stuart Mason7 min read

Every time I mention my hosting setup, someone asks "but how?" So here it is. The exact setup. No affiliate links, no sponsored nonsense. Just what works. ### Why Coolify Coolify is an open-source,

Deploying Laravel to Production with Coolify: My Exact Setup

Every time I mention my hosting setup, someone asks "but how?" So here it is. The exact setup. No affiliate links, no sponsored nonsense. Just what works.

Why Coolify

Coolify is an open-source, self-hosted alternative to platforms like Forge, Ploi, or Railway. You install it on a VPS and it manages your applications, databases, SSL certificates, and deployments. It's essentially a PaaS that runs on your own infrastructure.

Why not Forge? Forge is excellent, but it's another monthly bill per server. Coolify is free — you just pay for the VPS. At my scale (15+ apps), that saves a meaningful amount per year.

Why not Vapor? Because not everything needs to be serverless, and I like having a server I can SSH into when things go wrong. Call me old-fashioned.

Step 1: The VPS

I use Hetzner. A CX31 (4 vCPU, 8GB RAM) runs all 15+ apps comfortably for about 12 euros a month. If you're in the US, DigitalOcean or Vultr are comparable.

Requirements:

  • Ubuntu 22.04 or 24.04
  • At least 4GB RAM (Coolify itself uses about 1GB)
  • Root access

Step 2: Install Coolify

SSH in and run one command:

curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash

That's it. Coolify installs Docker, sets itself up, and gives you a web UI on port 8000. Create your admin account, point a domain at your server's IP (coolify.yourdomain.com), and configure SSL for the Coolify dashboard itself.

Step 3: Add a Server

In Coolify's UI, your VPS is already added as "localhost." For a single-server setup, you're done. Coolify can manage remote servers too, but for most people, running everything on one box is fine.

Step 4: Create the Laravel Application

In Coolify:

  1. Create a new Project (I group related apps together)
  2. Add a new Resource → Application
  3. Connect your GitHub account (OAuth)
  4. Select the repository and branch (main)
  5. Set the build pack to "Dockerfile" (not Nixpacks — I'll explain)

Step 5: The Dockerfile

I use a multi-stage Dockerfile for every Laravel app. This one handles PHP, Node (for Vite builds), and all the extensions a typical Laravel app needs:

FROM node:20-alpine AS node-build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM php:8.4-fpm-alpine AS php-base

RUN apk add --no-cache \
    nginx \
    supervisor \
    libpq-dev \
    libzip-dev \
    icu-dev \
    oniguruma-dev \
    && docker-php-ext-install \
    pdo_pgsql \
    pdo_mysql \
    zip \
    intl \
    mbstring \
    pcntl \
    bcmath \
    opcache

COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html

COPY . .
COPY --from=node-build /app/public/build public/build

RUN composer install --no-dev --optimize-autoloader --no-interaction

RUN php artisan config:cache \
    && php artisan route:cache \
    && php artisan view:cache

COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY docker/php.ini /usr/local/etc/php/conf.d/custom.ini

RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

EXPOSE 80

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

Why not Nixpacks? Nixpacks tries to auto-detect your stack and often gets it wrong for Laravel apps with specific extension requirements. A Dockerfile gives you full control and reproducible builds.

Step 6: Supervisor Config

Supervisor runs nginx, PHP-FPM, the queue worker, and the scheduler:

[supervisord]
nodaemon=true
user=root

[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true

[program:php-fpm]
command=php-fpm -F
autostart=true
autorestart=true

[program:queue-worker]
command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=1
user=www-data

[program:scheduler]
command=/bin/sh -c "while true; do php /var/www/html/artisan schedule:run --no-interaction; sleep 60; done"
autostart=true
autorestart=true
user=www-data

One container runs everything. For a high-traffic app you'd separate these, but for most Laravel apps this is absolutely fine.

Step 7: Nginx Config

server {
    listen 80;
    server_name _;
    root /var/www/html/public;
    index index.php;

    client_max_body_size 64M;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Standard Laravel nginx config. Nothing special.

Step 8: Environment Variables

In Coolify's application settings, add your environment variables. The critical ones:

APP_NAME="My App"
APP_ENV=production
APP_KEY=base64:...
APP_DEBUG=false
APP_URL=https://myapp.com

DB_CONNECTION=pgsql
DB_HOST=your-db-host
DB_PORT=5432
DB_DATABASE=myapp
DB_USERNAME=myapp
DB_PASSWORD=...

CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

REDIS_HOST=your-redis-host
REDIS_PORT=6379

For databases, I run PostgreSQL and Redis as separate Coolify resources. One PostgreSQL instance serves multiple apps (separate databases), same with Redis (separate prefixes).

Important: generate your APP_KEY locally with php artisan key:generate --show and paste it in. Don't run artisan commands on the production container for initial setup.

Step 9: SSL

Coolify handles SSL automatically via Let's Encrypt. In the application settings:

  1. Set your domain (e.g., myapp.com)
  2. Enable "Let's Encrypt" for SSL
  3. Coolify provisions and auto-renews the certificate

That's it. No certbot configuration, no cron jobs for renewal.

Step 10: Auto-Deploy on Merge

Coolify generates a webhook URL for each application. In GitHub:

  1. Go to your repo's Settings → Webhooks
  2. Add the Coolify webhook URL
  3. Set content type to application/json
  4. Select "Just the push event"
  5. Configure the branch filter in Coolify to only deploy from main

Now, merge a PR to main → GitHub fires the webhook → Coolify builds and deploys. Zero manual intervention. Typical deployment takes 2-3 minutes.

The laravel-coolify Package

I use the laravel-coolify package to add a health check endpoint and expose application info to Coolify:

composer require sevalla/laravel-coolify
// config/coolify.php
return [
    'health_check' => [
        'enabled' => true,
        'path' => '/health',
    ],
];

Coolify pings this endpoint to verify the application is running after deployment. If the health check fails, it rolls back to the previous version.

Database Migrations

I run migrations as part of the deployment process. In Coolify's "Post Deployment Command" setting:

php artisan migrate --force

The --force flag is required for production. If a migration fails, the deployment fails, and Coolify rolls back.

What It Costs

Monthly breakdown for running 15+ apps:

ItemCost
Hetzner CX31 (4 vCPU, 8GB RAM)~12 EUR
Hetzner Volume (40GB for databases)~2 EUR
Domain names (various)~5 EUR averaged
Total~19 EUR/month

That's roughly 16 quid. For context, a single Forge server costs $12/month on top of your VPS cost. Vapor's invocation costs for 15 apps would be considerably more. Even Railway or Render would cost 3-5x this.

The Trade-offs

What you give up: managed database backups (I use a cron job), one-click server scaling (I upgrade the VPS manually), and a support team to blame when things break.

What you get: full control, no vendor lock-in, predictable costs, and the ability to SSH in and fix things directly when it's 2am and something's broken.

Common Issues

Build fails with memory error: increase the Docker build memory limit in Coolify settings. Laravel's composer install with all dev dependencies can eat RAM.

Vite build fails: make sure your package-lock.json is committed. npm ci requires it.

Queue worker not processing: check that the supervisor config is included in the Docker build and that the redis connection is correct.

Health check failing: make sure the health check route isn't behind authentication middleware.

My Recommendation

If you're running 1-3 apps and don't want to think about infrastructure, use Forge. It's excellent and worth the money.

If you're running 5+ apps, or you want to understand your infrastructure, or you object to paying a monthly fee for something you can self-host, give Coolify a go. The initial setup takes a couple of hours. After that, deployments are automated and maintenance is minimal.

I've been running this setup for over two years. It's survived traffic spikes, botched deployments (rolled back automatically), and the occasional midnight debugging session. It's not glamorous, but it's reliable, cheap, and entirely under my control.


I write about Laravel, AI tooling, and the realities of building software. More at stuartmason.co.uk.

Get the Friday email

What I shipped this week, what I learned, one useful thing.

No spam. Unsubscribe anytime. Privacy policy.