**Tags:** "Hello World" Examples · PHP · Laravel

# Laravel showcase

A [Laravel](https://laravel.com) application connected to [PostgreSQL](https://www.postgresql.org/), [Valkey](https://valkey.io/) (Redis-compatible), S3-compatible object storage, and [Meilisearch](https://www.meilisearch.com/), running on [Zerops](https://zerops.io) with six ready-made environment configurations — from AI agent and remote development to stage and highly-available production.

### Available Environments

- **AI Agent** ← current
- [Remote (CDE)](https://app.zerops.io/recipes/laravel-showcase.md?environment=remote-cde)
- [Local](https://app.zerops.io/recipes/laravel-showcase.md?environment=local)
- [Stage](https://app.zerops.io/recipes/laravel-showcase.md?environment=stage)
- [Small Production](https://app.zerops.io/recipes/laravel-showcase.md?environment=small-production)
- [Highly-available Production](https://app.zerops.io/recipes/laravel-showcase.md?environment=highly-available-production)

### Services in this Environment

**Services:**

- **core** (core@1)
  - Containers: 1 × Shared Core, 0.00 GB RAM, 0 GB Disk
- **appdev** (php-nginx@8.4)
  - Containers: 1 × Shared Core, 0.56 GB RAM, 1 GB Disk
  - Repository: [zerops-recipe-apps/laravel-showcase-app](https://github.com/zerops-recipe-apps/laravel-showcase-app)
- **appstage** (php-nginx@8.4)
  - Containers: 1 × Shared Core, 0.56 GB RAM, 1 GB Disk
  - Repository: [zerops-recipe-apps/laravel-showcase-app](https://github.com/zerops-recipe-apps/laravel-showcase-app)
- **workerstage** (php-nginx@8.4)
  - Containers: 1 × Shared Core, 0.56 GB RAM, 1 GB Disk
  - Repository: [zerops-recipe-apps/laravel-showcase-app](https://github.com/zerops-recipe-apps/laravel-showcase-app)
- **db** (postgresql@18) :5432, :6432
  - Containers: 1 × Shared Core, 0.38 GB RAM, 1 GB Disk
- **redis** (valkey@7.2) :6379, :6380
  - Containers: 1 × Shared Core, 0.31 GB RAM, 1 GB Disk
- **storage** (object-storage)
  - Containers: 1 × Shared Core, 0.00 GB RAM, 0 GB Disk
- **search** (meilisearch@1.20) :7700
  - Containers: 1 × Shared Core, 0.31 GB RAM, 1 GB Disk

**Total Resources:** 8 containers, 2.69 GB RAM, 6 GB Disk

### One-Click Deploy (Import YAML)

Use this YAML with `zcli project import` to deploy this environment:

```yaml
#zeropsPreprocessor=on

# AI agent environment provides a development space for AI agents to build and
# version the app.
# It includes a dev service with the code repository and necessary development
# tools, a staging service, a low-resource database, a cache store, an object
# storage, and a search engine.

# APP_KEY is Laravel's AES-256-CBC encryption key — 32 random bytes generated
# by the preprocessor. Project-level so every container (app + worker) shares
# the same key, which is required for reading encrypted columns and validating
# session cookies written by any container behind the L7 balancer.
project:
  name: laravel-showcase-agent
  envVariables:
    APP_KEY: <@generateRandomString(<32>)>

services:
  # AI agent workspace — zeropsSetup:dev runs composer install (full dev deps)
  # and deploys the entire source tree. PHP-FPM reinterprets on every request so
  # the agent can edit files over SSHFS and see results instantly via the
  # subdomain URL. maxContainers:1 prevents file conflicts when editing over a
  # single mount point.
  - hostname: appdev
    type: php-nginx@8.4
    zeropsSetup: dev
    buildFromGit: https://github.com/zerops-recipe-apps/laravel-showcase-app
    enableSubdomainAccess: true
    verticalAutoscaling:
      minRam: 0.5

  # Staging slot for the agent — zeropsSetup:prod runs composer install
  # --no-dev, compiles Vite assets, and warms config/route/view caches. The
  # agent cross-deploys here to validate the production build pipeline before
  # finishing the task. readinessCheck gates traffic until the health endpoint
  # responds.
  - hostname: appstage
    type: php-nginx@8.4
    zeropsSetup: prod
    buildFromGit: https://github.com/zerops-recipe-apps/laravel-showcase-app
    enableSubdomainAccess: true
    verticalAutoscaling:
      minRam: 0.5

  # Background queue worker — zeropsSetup:worker runs composer install
  # --no-dev and starts artisan queue:work as the foreground process. Consumes
  # jobs dispatched by the app via Redis. No HTTP traffic, no healthCheck —
  # the process itself is the liveness signal.
  - hostname: workerstage
    type: php-nginx@8.4
    zeropsSetup: worker
    buildFromGit: https://github.com/zerops-recipe-apps/laravel-showcase-app
    verticalAutoscaling:
      minRam: 0.5

  # PostgreSQL — stores articles, sessions table (used by the database session
  # driver on first deploy before Redis takes over), and job/failed_job tables.
  # Shared by appdev, appstage, and workerstage. NON_HA is fine for an agent
  # workspace. Priority 10 ensures the database accepts connections before app
  # containers run migrations.
  - hostname: db
    type: postgresql@18
    priority: 10
    mode: NON_HA
    verticalAutoscaling:
      minRam: 0.25

  # Valkey (Redis-compatible) — handles cache, sessions, and queue in the
  # showcase tier. A single service replaces three separate database-backed
  # drivers, reducing latency for all three concerns. The predis PHP client
  # connects without a compiled C extension.
  - hostname: redis
    type: valkey@7.2
    priority: 10
    mode: NON_HA
    verticalAutoscaling:
      minRam: 0.25

  # S3-compatible object storage (MinIO backend) — the app uses Flysystem's S3
  # adapter with path-style endpoints for file uploads. Private policy keeps
  # uploaded files accessible only through the app.
  - hostname: storage
    type: object-storage
    priority: 10
    objectStorageSize: 1
    objectStoragePolicy: private

  # Meilisearch full-text search — Laravel Scout indexes Article models here.
  # The app queries it from the dashboard's live search feature. Internal HTTP
  # between services, no SSL.
  - hostname: search
    type: meilisearch@1.20
    priority: 10
    mode: NON_HA
    verticalAutoscaling:
      minRam: 0.25


```

---

## Next Steps

After deploying one of the environments and getting to know Zerops, you have two paths to choose from:

1. **Template Flow** — Clone our GitHub repositories and use the whole recipe as a template
2. **Integrate Flow** — If you already have an existing application on a similar stack, integrate the recipe setup with your application

Select a flow: [Template Flow](https://app.zerops.io/recipes/laravel-showcase.md?environment=ai-agent&guideFlow=template) or [Integrate Flow](https://app.zerops.io/recipes/laravel-showcase.md?environment=ai-agent&guideFlow=integrate)

Both flows are shown below:

## How to take over the AI Agent environment

### 📦 Clone the template repositories

Fork or clone the following repositories to your local machine or GitHub account:

- [zerops-recipe-apps/laravel-showcase-app](https://github.com/zerops-recipe-apps/laravel-showcase-app)
- [zerops-recipe-apps/laravel-showcase-app](https://github.com/zerops-recipe-apps/laravel-showcase-app)
- [zerops-recipe-apps/laravel-showcase-app](https://github.com/zerops-recipe-apps/laravel-showcase-app)

### Prerequisite: check authorization

After you deploy the AI Agent environment, an authorization dialog should appear.

Complete the Claude Code authorization before continuing. This is separate from your Zerops access token and uses your own Claude Code subscription or API credentials.

<img src="https://storage-prg1.zerops.io/4gfos-storage/quickstart_zcp_service_ready_863e8ba704.png" style="display: block; margin: 0 auto;" alt="Claude Code authorization dialog in Zerops" width="600" />

### 1. Open your ZCP workspace

The deployment creates your app services and a `zcp@1` workspace.

The agent, terminal, and browser IDE run inside the ZCP service. Your app still runs in the app services, not in the ZCP workspace.

<img src="https://storage-prg1.zerops.io/4gfos-storage/Screenshot_2026_03_10_at_01_19_49_e487f14e63.png" style="display: block; margin: 0 auto;" alt="ZCP workspace service in Zerops" width="600" />

### 2. Start in the browser IDE

The fastest way to work with the agent is to open the browser IDE from the ZCP service.

- No local setup is required.
- The agent runs close to your Zerops project.
- It can inspect project context and use available MCP tools.
- After the agent makes a change, open the app URL and verify the result.

<img src="https://storage-prg1.zerops.io/4gfos-storage/quickstart_cloud_ide_claude_code_f80940d851.jpg" style="display: block; margin: 0 auto;" alt="Browser IDE with Claude Code in Zerops" width="600" />

### 3. Connect from your local IDE or terminal

If you prefer local tooling, connect your machine to the Zerops project network.

Install zCLI and log in:

```bash
npm i -g @zerops/zcli
zcli login <personal-access-token>
```

Start the VPN:

```bash
zcli vpn up
```

Connect to the remote service:

```bash
ssh -A <service-name>.zerops
```

### 4. Work with the dev/stage flow

Use the development environment for fast iteration with the agent. Use staging to verify the result before moving toward production.

Typical flow:

1. Ask the agent for a focused change.
2. Review and test the result.
3. Push the change to your repository or deploy with `zcli push`.
4. Verify the result in staging.
5. Continue with the production guide when ready.

### 5. Move toward production

When the staging version is ready, finish the production setup:

- configure the deployment pipeline
- set up your domain
- check logs and runtime behavior
- configure backups if the recipe includes a database
- review scaling settings before real traffic arrives

## How to integrate appdev with Zerops

### 1. Adding `zerops.yaml`

The main configuration file — place at repository root. It tells Zerops how to build, deploy and run your app.

```yaml
zerops:
  # Production — optimized build, compiled assets, framework caches,
  # full service connectivity (DB, Redis, S3, Meilisearch).
  - setup: prod
    build:
      # Multi-base build: PHP for Composer, Node for Vite asset
      # compilation. Both runtimes are fully available on PATH
      # during the build — no manual install needed.
      base:
        - php@8.4
        - nodejs@22
      buildCommands:
        # Production Composer install — no dev packages, classmap
        # optimized for faster autoloading in production.
        - composer install --no-dev --optimize-autoloader
        # Vite compiles Tailwind CSS and JS into content-hashed
        # bundles in public/build/. These static assets are all
        # the runtime container needs from the Node side.
        - npm install
        - npm run build
      deployFiles:
        # List each directory explicitly — deploying ./ would
        # ship node_modules, .env.example, and other build-only
        # artifacts the runtime container doesn't need.
        - app
        - bootstrap
        - config
        - database
        - public
        - resources/views
        - routes
        - storage
        - vendor
        - artisan
        - composer.json
      # Cache vendor/ and node_modules/ between builds so
      # Composer and npm skip redundant network fetches.
      cache:
        - vendor
        - node_modules

    # Readiness check gates the traffic switch — new containers
    # must answer HTTP 200 before the L7 balancer routes to them.
    # This enables zero-downtime deploys.
    deploy:
      readinessCheck:
        httpGet:
          port: 80
          path: /health

    run:
      # php-nginx serves via Nginx + PHP-FPM — no explicit start
      # command needed; the base image handles both processes.
      base: php-nginx@8.4
      # Nginx serves static files from public/ and proxies PHP
      # requests to FPM. Laravel expects this as its web root.
      documentRoot: public
      # Config, route, and view caches MUST be built at runtime.
      # Build runs at /build/source/ but the app serves from
      # /var/www/ — caching during build bakes wrong paths.
      #
      # Migrations run exactly once per deploy via zsc execOnce,
      # regardless of how many containers start in parallel.
      # Seeder populates sample data on first deploy so the
      # dashboard shows real records immediately.
      # Scout import rebuilds the Meilisearch index from DB data
      # after seeding — the safety net for when auto-indexing
      # fires zero events (records already exist from prior deploy).
      initCommands:
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan migrate --force
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan db:seed --force
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan scout:import "App\\Models\\Article"
        - php artisan config:cache
        - php artisan route:cache
        - php artisan view:cache
      # Health check restarts unresponsive containers after the
      # 5-minute retry window expires — keeps production alive.
      healthCheck:
        httpGet:
          port: 80
          path: /health
      envVariables:
        APP_NAME: "Laravel Zerops"
        # Production mode — stack traces hidden, error pages
        # generic, optimizations enabled.
        APP_ENV: production
        APP_DEBUG: "false"
        # APP_URL drives absolute URL generation for redirects,
        # signed URLs, mail links, and CSRF origin validation.
        # zeropsSubdomain is the platform-injected HTTPS URL.
        APP_URL: ${zeropsSubdomain}
        # syslog routes Laravel logs to the Zerops runtime log
        # viewer via the local0 facility — no log files to
        # manage or rotate, and app logs stay tagged separately
        # from PHP-FPM/nginx system messages.
        LOG_CHANNEL: syslog
        LOG_SYSLOG_FACILITY: local0
        LOG_LEVEL: warning
        # Cross-service references resolve at deploy time.
        # Pattern: ${hostname_varname} maps to the db service's
        # auto-generated credentials.
        DB_CONNECTION: pgsql
        DB_HOST: ${db_hostname}
        DB_PORT: ${db_port}
        DB_DATABASE: ${db_dbName}
        DB_USERNAME: ${db_user}
        DB_PASSWORD: ${db_password}
        # Valkey (Redis-compatible) for cache, sessions, and
        # queues — single service handles all three concerns.
        # predis client is a pure-PHP Redis client that needs
        # no compiled extension.
        REDIS_CLIENT: predis
        REDIS_HOST: ${redis_hostname}
        REDIS_PORT: ${redis_port}
        SESSION_DRIVER: redis
        CACHE_STORE: redis
        QUEUE_CONNECTION: redis
        # S3-compatible object storage backed by MinIO.
        # forcePathStyle is mandatory — MinIO does not support
        # virtual-hosted bucket addressing.
        FILESYSTEM_DISK: s3
        AWS_ACCESS_KEY_ID: ${storage_accessKeyId}
        AWS_SECRET_ACCESS_KEY: ${storage_secretAccessKey}
        AWS_DEFAULT_REGION: us-east-1
        AWS_BUCKET: ${storage_bucketName}
        AWS_ENDPOINT: ${storage_apiUrl}
        AWS_USE_PATH_STYLE_ENDPOINT: "true"
        # Meilisearch for full-text search via Laravel Scout.
        # The host uses internal HTTP — SSL is terminated at
        # the L7 balancer, not between services.
        SCOUT_DRIVER: meilisearch
        MEILISEARCH_HOST: http://${search_hostname}:${search_port}
        MEILISEARCH_KEY: ${search_masterKey}
        # Mail set to log driver — no external SMTP configured.
        # Replace with real SMTP credentials for production use.
        MAIL_MAILER: log

  # Dev — full source deployed for live editing via SSHFS.
  # PHP-FPM serves requests immediately; edit files in /var/www
  # and changes take effect on the next request — no restart.
  - setup: dev
    build:
      # Same multi-base as prod — both PHP and Node available
      # during the build so Composer and npm can run.
      base:
        - php@8.4
        - nodejs@22
      buildCommands:
        # Full Composer install with dev packages — testing and
        # debugging tools available over SSH.
        - composer install
        # Pre-populate node_modules so the developer can run
        # npm run dev (Vite HMR) immediately after SSH-ing in
        # without waiting for another install.
        - npm install
      # Deploy the entire working directory — source files,
      # vendor/, node_modules/, and zerops.yaml so zcli push
      # works from the dev container.
      deployFiles:
        - ./
      cache:
        - vendor
        - node_modules

    run:
      base: php-nginx@8.4
      documentRoot: public
      # Install Node on the runtime container so the developer
      # can run Vite dev server (npm run dev) over SSH. This
      # runs once and is cached into the runtime image — not
      # re-executed on every container restart.
      prepareCommands:
        - sudo -E zsc install nodejs@22
      # Migration + seed runs once per deploy — DB is ready
      # when the SSH session starts. No cache warming in dev
      # — we want config changes to take effect immediately.
      initCommands:
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan migrate --force
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan db:seed --force
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan scout:import "App\\Models\\Article"
      envVariables:
        APP_NAME: "Laravel Zerops"
        # Dev mode — detailed error pages with stack traces,
        # no config caching, verbose logging for debugging.
        APP_ENV: local
        APP_DEBUG: "true"
        APP_URL: ${zeropsSubdomain}
        # Debug-level syslog logging surfaces all framework
        # events in the Zerops log viewer via local0 facility.
        LOG_CHANNEL: syslog
        LOG_SYSLOG_FACILITY: local0
        LOG_LEVEL: debug
        # Same service wiring as prod — only mode flags differ.
        DB_CONNECTION: pgsql
        DB_HOST: ${db_hostname}
        DB_PORT: ${db_port}
        DB_DATABASE: ${db_dbName}
        DB_USERNAME: ${db_user}
        DB_PASSWORD: ${db_password}
        REDIS_CLIENT: predis
        REDIS_HOST: ${redis_hostname}
        REDIS_PORT: ${redis_port}
        SESSION_DRIVER: redis
        CACHE_STORE: redis
        QUEUE_CONNECTION: redis
        FILESYSTEM_DISK: s3
        AWS_ACCESS_KEY_ID: ${storage_accessKeyId}
        AWS_SECRET_ACCESS_KEY: ${storage_secretAccessKey}
        AWS_DEFAULT_REGION: us-east-1
        AWS_BUCKET: ${storage_bucketName}
        AWS_ENDPOINT: ${storage_apiUrl}
        AWS_USE_PATH_STYLE_ENDPOINT: "true"
        SCOUT_DRIVER: meilisearch
        MEILISEARCH_HOST: http://${search_hostname}:${search_port}
        MEILISEARCH_KEY: ${search_masterKey}
        MAIL_MAILER: log

  # Worker — background job processor consuming from Redis queue.
  # Same codebase as the app, different entry point. No HTTP
  # traffic — no healthCheck, readinessCheck, or documentRoot.
  - setup: worker
    build:
      # Worker only needs PHP — no asset compilation. The queue
      # runner processes jobs, not HTTP requests with CSS/JS.
      base:
        - php@8.4
      buildCommands:
        - composer install --no-dev --optimize-autoloader
      deployFiles:
        - app
        - bootstrap
        - config
        - database
        - public
        - resources/views
        - routes
        - storage
        - vendor
        - artisan
        - composer.json
      cache:
        - vendor

    run:
      # php-nginx base provides the PHP runtime. The queue:work
      # command runs as the foreground process instead of FPM.
      base: php-nginx@8.4
      # artisan queue:work processes jobs from the Redis queue.
      # --sleep=3 polls every 3s when idle, --tries=3 retries
      # failed jobs before marking them as permanently failed.
      start: php artisan queue:work --sleep=3 --tries=3
      # Cache framework config on every container start so the
      # worker resolves env vars and service references correctly.
      initCommands:
        - php artisan config:cache
      envVariables:
        APP_NAME: "Laravel Zerops"
        APP_ENV: production
        APP_DEBUG: "false"
        APP_URL: ${zeropsSubdomain}
        LOG_CHANNEL: syslog
        LOG_SYSLOG_FACILITY: local0
        LOG_LEVEL: warning
        DB_CONNECTION: pgsql
        DB_HOST: ${db_hostname}
        DB_PORT: ${db_port}
        DB_DATABASE: ${db_dbName}
        DB_USERNAME: ${db_user}
        DB_PASSWORD: ${db_password}
        REDIS_CLIENT: predis
        REDIS_HOST: ${redis_hostname}
        REDIS_PORT: ${redis_port}
        # Worker shares the same Redis-backed drivers as the app.
        # Sessions are configured but unused by the CLI process.
        SESSION_DRIVER: redis
        CACHE_STORE: redis
        QUEUE_CONNECTION: redis
        FILESYSTEM_DISK: s3
        AWS_ACCESS_KEY_ID: ${storage_accessKeyId}
        AWS_SECRET_ACCESS_KEY: ${storage_secretAccessKey}
        AWS_DEFAULT_REGION: us-east-1
        AWS_BUCKET: ${storage_bucketName}
        AWS_ENDPOINT: ${storage_apiUrl}
        AWS_USE_PATH_STYLE_ENDPOINT: "true"
        SCOUT_DRIVER: meilisearch
        MEILISEARCH_HOST: http://${search_hostname}:${search_port}
        MEILISEARCH_KEY: ${search_masterKey}
        MAIL_MAILER: log
```

### 2. Trust the reverse proxy

Zerops terminates SSL at its L7 balancer and forwards requests via reverse proxy. Without trusting the proxy, Laravel rejects CSRF tokens and generates `http://` URLs instead of `https://`. In `bootstrap/app.php`:

```php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->trustProxies(at: '*');
})
```

### 3. Configure Redis client

Laravel defaults to the `phpredis` C extension. On Zerops, the `predis` pure-PHP client avoids needing a compiled extension. Install via Composer and set `REDIS_CLIENT=predis` in your environment:

```bash
composer require predis/predis
```

### 4. Configure S3 object storage

Install the S3 Flysystem adapter and set `FILESYSTEM_DISK=s3` with the Zerops object storage credentials. Path-style endpoints are mandatory for the MinIO-backed storage:

```bash
composer require league/flysystem-aws-s3-v3
```

### 5. Configure Meilisearch search

Install Laravel Scout with the Meilisearch driver for full-text search. Add the `Searchable` trait to models you want indexed:

```bash
composer require laravel/scout meilisearch/meilisearch-php
```

## How to integrate appstage with Zerops

### 1. Adding `zerops.yaml`

The main configuration file — place at repository root. It tells Zerops how to build, deploy and run your app.

```yaml
zerops:
  # Production — optimized build, compiled assets, framework caches,
  # full service connectivity (DB, Redis, S3, Meilisearch).
  - setup: prod
    build:
      # Multi-base build: PHP for Composer, Node for Vite asset
      # compilation. Both runtimes are fully available on PATH
      # during the build — no manual install needed.
      base:
        - php@8.4
        - nodejs@22
      buildCommands:
        # Production Composer install — no dev packages, classmap
        # optimized for faster autoloading in production.
        - composer install --no-dev --optimize-autoloader
        # Vite compiles Tailwind CSS and JS into content-hashed
        # bundles in public/build/. These static assets are all
        # the runtime container needs from the Node side.
        - npm install
        - npm run build
      deployFiles:
        # List each directory explicitly — deploying ./ would
        # ship node_modules, .env.example, and other build-only
        # artifacts the runtime container doesn't need.
        - app
        - bootstrap
        - config
        - database
        - public
        - resources/views
        - routes
        - storage
        - vendor
        - artisan
        - composer.json
      # Cache vendor/ and node_modules/ between builds so
      # Composer and npm skip redundant network fetches.
      cache:
        - vendor
        - node_modules

    # Readiness check gates the traffic switch — new containers
    # must answer HTTP 200 before the L7 balancer routes to them.
    # This enables zero-downtime deploys.
    deploy:
      readinessCheck:
        httpGet:
          port: 80
          path: /health

    run:
      # php-nginx serves via Nginx + PHP-FPM — no explicit start
      # command needed; the base image handles both processes.
      base: php-nginx@8.4
      # Nginx serves static files from public/ and proxies PHP
      # requests to FPM. Laravel expects this as its web root.
      documentRoot: public
      # Config, route, and view caches MUST be built at runtime.
      # Build runs at /build/source/ but the app serves from
      # /var/www/ — caching during build bakes wrong paths.
      #
      # Migrations run exactly once per deploy via zsc execOnce,
      # regardless of how many containers start in parallel.
      # Seeder populates sample data on first deploy so the
      # dashboard shows real records immediately.
      # Scout import rebuilds the Meilisearch index from DB data
      # after seeding — the safety net for when auto-indexing
      # fires zero events (records already exist from prior deploy).
      initCommands:
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan migrate --force
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan db:seed --force
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan scout:import "App\\Models\\Article"
        - php artisan config:cache
        - php artisan route:cache
        - php artisan view:cache
      # Health check restarts unresponsive containers after the
      # 5-minute retry window expires — keeps production alive.
      healthCheck:
        httpGet:
          port: 80
          path: /health
      envVariables:
        APP_NAME: "Laravel Zerops"
        # Production mode — stack traces hidden, error pages
        # generic, optimizations enabled.
        APP_ENV: production
        APP_DEBUG: "false"
        # APP_URL drives absolute URL generation for redirects,
        # signed URLs, mail links, and CSRF origin validation.
        # zeropsSubdomain is the platform-injected HTTPS URL.
        APP_URL: ${zeropsSubdomain}
        # syslog routes Laravel logs to the Zerops runtime log
        # viewer via the local0 facility — no log files to
        # manage or rotate, and app logs stay tagged separately
        # from PHP-FPM/nginx system messages.
        LOG_CHANNEL: syslog
        LOG_SYSLOG_FACILITY: local0
        LOG_LEVEL: warning
        # Cross-service references resolve at deploy time.
        # Pattern: ${hostname_varname} maps to the db service's
        # auto-generated credentials.
        DB_CONNECTION: pgsql
        DB_HOST: ${db_hostname}
        DB_PORT: ${db_port}
        DB_DATABASE: ${db_dbName}
        DB_USERNAME: ${db_user}
        DB_PASSWORD: ${db_password}
        # Valkey (Redis-compatible) for cache, sessions, and
        # queues — single service handles all three concerns.
        # predis client is a pure-PHP Redis client that needs
        # no compiled extension.
        REDIS_CLIENT: predis
        REDIS_HOST: ${redis_hostname}
        REDIS_PORT: ${redis_port}
        SESSION_DRIVER: redis
        CACHE_STORE: redis
        QUEUE_CONNECTION: redis
        # S3-compatible object storage backed by MinIO.
        # forcePathStyle is mandatory — MinIO does not support
        # virtual-hosted bucket addressing.
        FILESYSTEM_DISK: s3
        AWS_ACCESS_KEY_ID: ${storage_accessKeyId}
        AWS_SECRET_ACCESS_KEY: ${storage_secretAccessKey}
        AWS_DEFAULT_REGION: us-east-1
        AWS_BUCKET: ${storage_bucketName}
        AWS_ENDPOINT: ${storage_apiUrl}
        AWS_USE_PATH_STYLE_ENDPOINT: "true"
        # Meilisearch for full-text search via Laravel Scout.
        # The host uses internal HTTP — SSL is terminated at
        # the L7 balancer, not between services.
        SCOUT_DRIVER: meilisearch
        MEILISEARCH_HOST: http://${search_hostname}:${search_port}
        MEILISEARCH_KEY: ${search_masterKey}
        # Mail set to log driver — no external SMTP configured.
        # Replace with real SMTP credentials for production use.
        MAIL_MAILER: log

  # Dev — full source deployed for live editing via SSHFS.
  # PHP-FPM serves requests immediately; edit files in /var/www
  # and changes take effect on the next request — no restart.
  - setup: dev
    build:
      # Same multi-base as prod — both PHP and Node available
      # during the build so Composer and npm can run.
      base:
        - php@8.4
        - nodejs@22
      buildCommands:
        # Full Composer install with dev packages — testing and
        # debugging tools available over SSH.
        - composer install
        # Pre-populate node_modules so the developer can run
        # npm run dev (Vite HMR) immediately after SSH-ing in
        # without waiting for another install.
        - npm install
      # Deploy the entire working directory — source files,
      # vendor/, node_modules/, and zerops.yaml so zcli push
      # works from the dev container.
      deployFiles:
        - ./
      cache:
        - vendor
        - node_modules

    run:
      base: php-nginx@8.4
      documentRoot: public
      # Install Node on the runtime container so the developer
      # can run Vite dev server (npm run dev) over SSH. This
      # runs once and is cached into the runtime image — not
      # re-executed on every container restart.
      prepareCommands:
        - sudo -E zsc install nodejs@22
      # Migration + seed runs once per deploy — DB is ready
      # when the SSH session starts. No cache warming in dev
      # — we want config changes to take effect immediately.
      initCommands:
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan migrate --force
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan db:seed --force
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan scout:import "App\\Models\\Article"
      envVariables:
        APP_NAME: "Laravel Zerops"
        # Dev mode — detailed error pages with stack traces,
        # no config caching, verbose logging for debugging.
        APP_ENV: local
        APP_DEBUG: "true"
        APP_URL: ${zeropsSubdomain}
        # Debug-level syslog logging surfaces all framework
        # events in the Zerops log viewer via local0 facility.
        LOG_CHANNEL: syslog
        LOG_SYSLOG_FACILITY: local0
        LOG_LEVEL: debug
        # Same service wiring as prod — only mode flags differ.
        DB_CONNECTION: pgsql
        DB_HOST: ${db_hostname}
        DB_PORT: ${db_port}
        DB_DATABASE: ${db_dbName}
        DB_USERNAME: ${db_user}
        DB_PASSWORD: ${db_password}
        REDIS_CLIENT: predis
        REDIS_HOST: ${redis_hostname}
        REDIS_PORT: ${redis_port}
        SESSION_DRIVER: redis
        CACHE_STORE: redis
        QUEUE_CONNECTION: redis
        FILESYSTEM_DISK: s3
        AWS_ACCESS_KEY_ID: ${storage_accessKeyId}
        AWS_SECRET_ACCESS_KEY: ${storage_secretAccessKey}
        AWS_DEFAULT_REGION: us-east-1
        AWS_BUCKET: ${storage_bucketName}
        AWS_ENDPOINT: ${storage_apiUrl}
        AWS_USE_PATH_STYLE_ENDPOINT: "true"
        SCOUT_DRIVER: meilisearch
        MEILISEARCH_HOST: http://${search_hostname}:${search_port}
        MEILISEARCH_KEY: ${search_masterKey}
        MAIL_MAILER: log

  # Worker — background job processor consuming from Redis queue.
  # Same codebase as the app, different entry point. No HTTP
  # traffic — no healthCheck, readinessCheck, or documentRoot.
  - setup: worker
    build:
      # Worker only needs PHP — no asset compilation. The queue
      # runner processes jobs, not HTTP requests with CSS/JS.
      base:
        - php@8.4
      buildCommands:
        - composer install --no-dev --optimize-autoloader
      deployFiles:
        - app
        - bootstrap
        - config
        - database
        - public
        - resources/views
        - routes
        - storage
        - vendor
        - artisan
        - composer.json
      cache:
        - vendor

    run:
      # php-nginx base provides the PHP runtime. The queue:work
      # command runs as the foreground process instead of FPM.
      base: php-nginx@8.4
      # artisan queue:work processes jobs from the Redis queue.
      # --sleep=3 polls every 3s when idle, --tries=3 retries
      # failed jobs before marking them as permanently failed.
      start: php artisan queue:work --sleep=3 --tries=3
      # Cache framework config on every container start so the
      # worker resolves env vars and service references correctly.
      initCommands:
        - php artisan config:cache
      envVariables:
        APP_NAME: "Laravel Zerops"
        APP_ENV: production
        APP_DEBUG: "false"
        APP_URL: ${zeropsSubdomain}
        LOG_CHANNEL: syslog
        LOG_SYSLOG_FACILITY: local0
        LOG_LEVEL: warning
        DB_CONNECTION: pgsql
        DB_HOST: ${db_hostname}
        DB_PORT: ${db_port}
        DB_DATABASE: ${db_dbName}
        DB_USERNAME: ${db_user}
        DB_PASSWORD: ${db_password}
        REDIS_CLIENT: predis
        REDIS_HOST: ${redis_hostname}
        REDIS_PORT: ${redis_port}
        # Worker shares the same Redis-backed drivers as the app.
        # Sessions are configured but unused by the CLI process.
        SESSION_DRIVER: redis
        CACHE_STORE: redis
        QUEUE_CONNECTION: redis
        FILESYSTEM_DISK: s3
        AWS_ACCESS_KEY_ID: ${storage_accessKeyId}
        AWS_SECRET_ACCESS_KEY: ${storage_secretAccessKey}
        AWS_DEFAULT_REGION: us-east-1
        AWS_BUCKET: ${storage_bucketName}
        AWS_ENDPOINT: ${storage_apiUrl}
        AWS_USE_PATH_STYLE_ENDPOINT: "true"
        SCOUT_DRIVER: meilisearch
        MEILISEARCH_HOST: http://${search_hostname}:${search_port}
        MEILISEARCH_KEY: ${search_masterKey}
        MAIL_MAILER: log
```

### 2. Trust the reverse proxy

Zerops terminates SSL at its L7 balancer and forwards requests via reverse proxy. Without trusting the proxy, Laravel rejects CSRF tokens and generates `http://` URLs instead of `https://`. In `bootstrap/app.php`:

```php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->trustProxies(at: '*');
})
```

### 3. Configure Redis client

Laravel defaults to the `phpredis` C extension. On Zerops, the `predis` pure-PHP client avoids needing a compiled extension. Install via Composer and set `REDIS_CLIENT=predis` in your environment:

```bash
composer require predis/predis
```

### 4. Configure S3 object storage

Install the S3 Flysystem adapter and set `FILESYSTEM_DISK=s3` with the Zerops object storage credentials. Path-style endpoints are mandatory for the MinIO-backed storage:

```bash
composer require league/flysystem-aws-s3-v3
```

### 5. Configure Meilisearch search

Install Laravel Scout with the Meilisearch driver for full-text search. Add the `Searchable` trait to models you want indexed:

```bash
composer require laravel/scout meilisearch/meilisearch-php
```

## How to integrate workerstage with Zerops

### 1. Adding `zerops.yaml`

The main configuration file — place at repository root. It tells Zerops how to build, deploy and run your app.

```yaml
zerops:
  # Production — optimized build, compiled assets, framework caches,
  # full service connectivity (DB, Redis, S3, Meilisearch).
  - setup: prod
    build:
      # Multi-base build: PHP for Composer, Node for Vite asset
      # compilation. Both runtimes are fully available on PATH
      # during the build — no manual install needed.
      base:
        - php@8.4
        - nodejs@22
      buildCommands:
        # Production Composer install — no dev packages, classmap
        # optimized for faster autoloading in production.
        - composer install --no-dev --optimize-autoloader
        # Vite compiles Tailwind CSS and JS into content-hashed
        # bundles in public/build/. These static assets are all
        # the runtime container needs from the Node side.
        - npm install
        - npm run build
      deployFiles:
        # List each directory explicitly — deploying ./ would
        # ship node_modules, .env.example, and other build-only
        # artifacts the runtime container doesn't need.
        - app
        - bootstrap
        - config
        - database
        - public
        - resources/views
        - routes
        - storage
        - vendor
        - artisan
        - composer.json
      # Cache vendor/ and node_modules/ between builds so
      # Composer and npm skip redundant network fetches.
      cache:
        - vendor
        - node_modules

    # Readiness check gates the traffic switch — new containers
    # must answer HTTP 200 before the L7 balancer routes to them.
    # This enables zero-downtime deploys.
    deploy:
      readinessCheck:
        httpGet:
          port: 80
          path: /health

    run:
      # php-nginx serves via Nginx + PHP-FPM — no explicit start
      # command needed; the base image handles both processes.
      base: php-nginx@8.4
      # Nginx serves static files from public/ and proxies PHP
      # requests to FPM. Laravel expects this as its web root.
      documentRoot: public
      # Config, route, and view caches MUST be built at runtime.
      # Build runs at /build/source/ but the app serves from
      # /var/www/ — caching during build bakes wrong paths.
      #
      # Migrations run exactly once per deploy via zsc execOnce,
      # regardless of how many containers start in parallel.
      # Seeder populates sample data on first deploy so the
      # dashboard shows real records immediately.
      # Scout import rebuilds the Meilisearch index from DB data
      # after seeding — the safety net for when auto-indexing
      # fires zero events (records already exist from prior deploy).
      initCommands:
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan migrate --force
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan db:seed --force
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan scout:import "App\\Models\\Article"
        - php artisan config:cache
        - php artisan route:cache
        - php artisan view:cache
      # Health check restarts unresponsive containers after the
      # 5-minute retry window expires — keeps production alive.
      healthCheck:
        httpGet:
          port: 80
          path: /health
      envVariables:
        APP_NAME: "Laravel Zerops"
        # Production mode — stack traces hidden, error pages
        # generic, optimizations enabled.
        APP_ENV: production
        APP_DEBUG: "false"
        # APP_URL drives absolute URL generation for redirects,
        # signed URLs, mail links, and CSRF origin validation.
        # zeropsSubdomain is the platform-injected HTTPS URL.
        APP_URL: ${zeropsSubdomain}
        # syslog routes Laravel logs to the Zerops runtime log
        # viewer via the local0 facility — no log files to
        # manage or rotate, and app logs stay tagged separately
        # from PHP-FPM/nginx system messages.
        LOG_CHANNEL: syslog
        LOG_SYSLOG_FACILITY: local0
        LOG_LEVEL: warning
        # Cross-service references resolve at deploy time.
        # Pattern: ${hostname_varname} maps to the db service's
        # auto-generated credentials.
        DB_CONNECTION: pgsql
        DB_HOST: ${db_hostname}
        DB_PORT: ${db_port}
        DB_DATABASE: ${db_dbName}
        DB_USERNAME: ${db_user}
        DB_PASSWORD: ${db_password}
        # Valkey (Redis-compatible) for cache, sessions, and
        # queues — single service handles all three concerns.
        # predis client is a pure-PHP Redis client that needs
        # no compiled extension.
        REDIS_CLIENT: predis
        REDIS_HOST: ${redis_hostname}
        REDIS_PORT: ${redis_port}
        SESSION_DRIVER: redis
        CACHE_STORE: redis
        QUEUE_CONNECTION: redis
        # S3-compatible object storage backed by MinIO.
        # forcePathStyle is mandatory — MinIO does not support
        # virtual-hosted bucket addressing.
        FILESYSTEM_DISK: s3
        AWS_ACCESS_KEY_ID: ${storage_accessKeyId}
        AWS_SECRET_ACCESS_KEY: ${storage_secretAccessKey}
        AWS_DEFAULT_REGION: us-east-1
        AWS_BUCKET: ${storage_bucketName}
        AWS_ENDPOINT: ${storage_apiUrl}
        AWS_USE_PATH_STYLE_ENDPOINT: "true"
        # Meilisearch for full-text search via Laravel Scout.
        # The host uses internal HTTP — SSL is terminated at
        # the L7 balancer, not between services.
        SCOUT_DRIVER: meilisearch
        MEILISEARCH_HOST: http://${search_hostname}:${search_port}
        MEILISEARCH_KEY: ${search_masterKey}
        # Mail set to log driver — no external SMTP configured.
        # Replace with real SMTP credentials for production use.
        MAIL_MAILER: log

  # Dev — full source deployed for live editing via SSHFS.
  # PHP-FPM serves requests immediately; edit files in /var/www
  # and changes take effect on the next request — no restart.
  - setup: dev
    build:
      # Same multi-base as prod — both PHP and Node available
      # during the build so Composer and npm can run.
      base:
        - php@8.4
        - nodejs@22
      buildCommands:
        # Full Composer install with dev packages — testing and
        # debugging tools available over SSH.
        - composer install
        # Pre-populate node_modules so the developer can run
        # npm run dev (Vite HMR) immediately after SSH-ing in
        # without waiting for another install.
        - npm install
      # Deploy the entire working directory — source files,
      # vendor/, node_modules/, and zerops.yaml so zcli push
      # works from the dev container.
      deployFiles:
        - ./
      cache:
        - vendor
        - node_modules

    run:
      base: php-nginx@8.4
      documentRoot: public
      # Install Node on the runtime container so the developer
      # can run Vite dev server (npm run dev) over SSH. This
      # runs once and is cached into the runtime image — not
      # re-executed on every container restart.
      prepareCommands:
        - sudo -E zsc install nodejs@22
      # Migration + seed runs once per deploy — DB is ready
      # when the SSH session starts. No cache warming in dev
      # — we want config changes to take effect immediately.
      initCommands:
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan migrate --force
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan db:seed --force
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- php artisan scout:import "App\\Models\\Article"
      envVariables:
        APP_NAME: "Laravel Zerops"
        # Dev mode — detailed error pages with stack traces,
        # no config caching, verbose logging for debugging.
        APP_ENV: local
        APP_DEBUG: "true"
        APP_URL: ${zeropsSubdomain}
        # Debug-level syslog logging surfaces all framework
        # events in the Zerops log viewer via local0 facility.
        LOG_CHANNEL: syslog
        LOG_SYSLOG_FACILITY: local0
        LOG_LEVEL: debug
        # Same service wiring as prod — only mode flags differ.
        DB_CONNECTION: pgsql
        DB_HOST: ${db_hostname}
        DB_PORT: ${db_port}
        DB_DATABASE: ${db_dbName}
        DB_USERNAME: ${db_user}
        DB_PASSWORD: ${db_password}
        REDIS_CLIENT: predis
        REDIS_HOST: ${redis_hostname}
        REDIS_PORT: ${redis_port}
        SESSION_DRIVER: redis
        CACHE_STORE: redis
        QUEUE_CONNECTION: redis
        FILESYSTEM_DISK: s3
        AWS_ACCESS_KEY_ID: ${storage_accessKeyId}
        AWS_SECRET_ACCESS_KEY: ${storage_secretAccessKey}
        AWS_DEFAULT_REGION: us-east-1
        AWS_BUCKET: ${storage_bucketName}
        AWS_ENDPOINT: ${storage_apiUrl}
        AWS_USE_PATH_STYLE_ENDPOINT: "true"
        SCOUT_DRIVER: meilisearch
        MEILISEARCH_HOST: http://${search_hostname}:${search_port}
        MEILISEARCH_KEY: ${search_masterKey}
        MAIL_MAILER: log

  # Worker — background job processor consuming from Redis queue.
  # Same codebase as the app, different entry point. No HTTP
  # traffic — no healthCheck, readinessCheck, or documentRoot.
  - setup: worker
    build:
      # Worker only needs PHP — no asset compilation. The queue
      # runner processes jobs, not HTTP requests with CSS/JS.
      base:
        - php@8.4
      buildCommands:
        - composer install --no-dev --optimize-autoloader
      deployFiles:
        - app
        - bootstrap
        - config
        - database
        - public
        - resources/views
        - routes
        - storage
        - vendor
        - artisan
        - composer.json
      cache:
        - vendor

    run:
      # php-nginx base provides the PHP runtime. The queue:work
      # command runs as the foreground process instead of FPM.
      base: php-nginx@8.4
      # artisan queue:work processes jobs from the Redis queue.
      # --sleep=3 polls every 3s when idle, --tries=3 retries
      # failed jobs before marking them as permanently failed.
      start: php artisan queue:work --sleep=3 --tries=3
      # Cache framework config on every container start so the
      # worker resolves env vars and service references correctly.
      initCommands:
        - php artisan config:cache
      envVariables:
        APP_NAME: "Laravel Zerops"
        APP_ENV: production
        APP_DEBUG: "false"
        APP_URL: ${zeropsSubdomain}
        LOG_CHANNEL: syslog
        LOG_SYSLOG_FACILITY: local0
        LOG_LEVEL: warning
        DB_CONNECTION: pgsql
        DB_HOST: ${db_hostname}
        DB_PORT: ${db_port}
        DB_DATABASE: ${db_dbName}
        DB_USERNAME: ${db_user}
        DB_PASSWORD: ${db_password}
        REDIS_CLIENT: predis
        REDIS_HOST: ${redis_hostname}
        REDIS_PORT: ${redis_port}
        # Worker shares the same Redis-backed drivers as the app.
        # Sessions are configured but unused by the CLI process.
        SESSION_DRIVER: redis
        CACHE_STORE: redis
        QUEUE_CONNECTION: redis
        FILESYSTEM_DISK: s3
        AWS_ACCESS_KEY_ID: ${storage_accessKeyId}
        AWS_SECRET_ACCESS_KEY: ${storage_secretAccessKey}
        AWS_DEFAULT_REGION: us-east-1
        AWS_BUCKET: ${storage_bucketName}
        AWS_ENDPOINT: ${storage_apiUrl}
        AWS_USE_PATH_STYLE_ENDPOINT: "true"
        SCOUT_DRIVER: meilisearch
        MEILISEARCH_HOST: http://${search_hostname}:${search_port}
        MEILISEARCH_KEY: ${search_masterKey}
        MAIL_MAILER: log
```

### 2. Trust the reverse proxy

Zerops terminates SSL at its L7 balancer and forwards requests via reverse proxy. Without trusting the proxy, Laravel rejects CSRF tokens and generates `http://` URLs instead of `https://`. In `bootstrap/app.php`:

```php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->trustProxies(at: '*');
})
```

### 3. Configure Redis client

Laravel defaults to the `phpredis` C extension. On Zerops, the `predis` pure-PHP client avoids needing a compiled extension. Install via Composer and set `REDIS_CLIENT=predis` in your environment:

```bash
composer require predis/predis
```

### 4. Configure S3 object storage

Install the S3 Flysystem adapter and set `FILESYSTEM_DISK=s3` with the Zerops object storage credentials. Path-style endpoints are mandatory for the MinIO-backed storage:

```bash
composer require league/flysystem-aws-s3-v3
```

### 5. Configure Meilisearch search

Install Laravel Scout with the Meilisearch driver for full-text search. Add the `Searchable` trait to models you want indexed:

```bash
composer require laravel/scout meilisearch/meilisearch-php
```

### 🎯 What's next?

**Deploy other environments** — Ready to scale? Deploy additional environments for different stages of your workflow:

- [Remote (CDE)](https://app.zerops.io/recipes/laravel-showcase.md?environment=remote-cde)
- [Local](https://app.zerops.io/recipes/laravel-showcase.md?environment=local)
- [Stage](https://app.zerops.io/recipes/laravel-showcase.md?environment=stage)
- [Small Production](https://app.zerops.io/recipes/laravel-showcase.md?environment=small-production)
- [Highly-available Production](https://app.zerops.io/recipes/laravel-showcase.md?environment=highly-available-production)

## Knowledge Base

### Platform Reference

- [Routing & Domains](https://docs.zerops.io/features/access)
- [Scaling](https://docs.zerops.io/features/scaling)
- [Environment Variables](https://docs.zerops.io/features/env-variables)
- [CLI (zcli)](https://docs.zerops.io/references/cli)

### Service Type Reference

**PHP+Nginx**

- [Build & Deploy](https://docs.zerops.io/nginx/how-to/build-pipeline)
- [Customize Web Server](https://docs.zerops.io/nginx/how-to/customize-web-server)
- [SEO Setup](https://docs.zerops.io/nginx/how-to/customize-web-server#seo--prerender-support)

**PostgreSQL**

- [Connect](https://docs.zerops.io/postgresql/how-to/connect)
- [Backup & Restore](https://docs.zerops.io/postgresql/how-to/backup)
- [Manage](https://docs.zerops.io/postgresql/how-to/manage)
- [Scale](https://docs.zerops.io/postgresql/how-to/scale)

**Valkey**

- [Configuration & Access](https://docs.zerops.io/valkey/overview#service-configuration)

**Meilisearch**

- [Configuration](https://docs.zerops.io/meilisearch/overview#service-configuration)
- [Access](https://docs.zerops.io/typesense/overview#access-methods)
- [Backup & Restore](https://docs.zerops.io/typesense/overview#backup)

### Application Reference

#### appdev Knowledge Base

### Gotchas

- **No `.env` file** — Zerops injects environment variables as OS env vars. Creating a `.env` file with empty values shadows the OS vars, causing `env()` to return `null` for every key that appears in `.env` even if the platform has a value set.
- **Cache commands in `initCommands`, not `buildCommands`** — `config:cache`, `route:cache`, and `view:cache` bake absolute paths into their cached files. The build container runs at `/build/source/` while the runtime serves from `/var/www/`. Caching during build produces paths like `/build/source/storage/...` that crash at runtime with "directory not found."
- **`APP_KEY` is project-level** — Laravel's encryption key must be shared across all services that read the same database (app + worker both need the same key for sessions and encrypted columns). Set it once at project level in Zerops; do not add it per-service or in `zerops.yaml envVariables`.
- **PDO PostgreSQL extension** — The `php-nginx` base image includes `pdo_pgsql` out of the box. No `prepareCommands` or `apk add` needed for PostgreSQL connectivity.
- **Predis over phpredis** — The `php-nginx` base image does not include the `phpredis` C extension. Use the `predis/predis` Composer package and set `REDIS_CLIENT=predis` to avoid "class Redis not found" errors.
- **Object storage requires path-style** — Zerops object storage uses MinIO, which requires `AWS_USE_PATH_STYLE_ENDPOINT=true`. Without it, the SDK attempts virtual-hosted bucket URLs that MinIO cannot resolve.
- **Vite manifest missing on dev after fresh deploy** — the `dev` setup intentionally omits `npm run build` from `buildCommands` so the HMR workflow (`npm run dev` via SSH) stays fast. Any view rendering `@vite(...)` therefore 500s with `Vite manifest not found at: /var/www/public/build/manifest.json` on the first request after a `zerops_deploy`. Fix: run `ssh appdev 'cd /var/www && npm run build'` once after the deploy and before `zerops_verify` — SSHFS propagates the manifest into the container without a redeploy. For iterative work, `ssh appdev 'cd /var/www && nohup npm run dev > /tmp/vite.log 2>&1 &'` drops `public/build/hot` and Laravel routes asset URLs to the dev server. **Do NOT add `npm run build` to dev `buildCommands`** — it adds ~20–30 s to every `zcli push` and defeats the HMR-first design.

#### appstage Knowledge Base

### Gotchas

- **No `.env` file** — Zerops injects environment variables as OS env vars. Creating a `.env` file with empty values shadows the OS vars, causing `env()` to return `null` for every key that appears in `.env` even if the platform has a value set.
- **Cache commands in `initCommands`, not `buildCommands`** — `config:cache`, `route:cache`, and `view:cache` bake absolute paths into their cached files. The build container runs at `/build/source/` while the runtime serves from `/var/www/`. Caching during build produces paths like `/build/source/storage/...` that crash at runtime with "directory not found."
- **`APP_KEY` is project-level** — Laravel's encryption key must be shared across all services that read the same database (app + worker both need the same key for sessions and encrypted columns). Set it once at project level in Zerops; do not add it per-service or in `zerops.yaml envVariables`.
- **PDO PostgreSQL extension** — The `php-nginx` base image includes `pdo_pgsql` out of the box. No `prepareCommands` or `apk add` needed for PostgreSQL connectivity.
- **Predis over phpredis** — The `php-nginx` base image does not include the `phpredis` C extension. Use the `predis/predis` Composer package and set `REDIS_CLIENT=predis` to avoid "class Redis not found" errors.
- **Object storage requires path-style** — Zerops object storage uses MinIO, which requires `AWS_USE_PATH_STYLE_ENDPOINT=true`. Without it, the SDK attempts virtual-hosted bucket URLs that MinIO cannot resolve.
- **Vite manifest missing on dev after fresh deploy** — the `dev` setup intentionally omits `npm run build` from `buildCommands` so the HMR workflow (`npm run dev` via SSH) stays fast. Any view rendering `@vite(...)` therefore 500s with `Vite manifest not found at: /var/www/public/build/manifest.json` on the first request after a `zerops_deploy`. Fix: run `ssh appdev 'cd /var/www && npm run build'` once after the deploy and before `zerops_verify` — SSHFS propagates the manifest into the container without a redeploy. For iterative work, `ssh appdev 'cd /var/www && nohup npm run dev > /tmp/vite.log 2>&1 &'` drops `public/build/hot` and Laravel routes asset URLs to the dev server. **Do NOT add `npm run build` to dev `buildCommands`** — it adds ~20–30 s to every `zcli push` and defeats the HMR-first design.

#### workerstage Knowledge Base

### Gotchas

- **No `.env` file** — Zerops injects environment variables as OS env vars. Creating a `.env` file with empty values shadows the OS vars, causing `env()` to return `null` for every key that appears in `.env` even if the platform has a value set.
- **Cache commands in `initCommands`, not `buildCommands`** — `config:cache`, `route:cache`, and `view:cache` bake absolute paths into their cached files. The build container runs at `/build/source/` while the runtime serves from `/var/www/`. Caching during build produces paths like `/build/source/storage/...` that crash at runtime with "directory not found."
- **`APP_KEY` is project-level** — Laravel's encryption key must be shared across all services that read the same database (app + worker both need the same key for sessions and encrypted columns). Set it once at project level in Zerops; do not add it per-service or in `zerops.yaml envVariables`.
- **PDO PostgreSQL extension** — The `php-nginx` base image includes `pdo_pgsql` out of the box. No `prepareCommands` or `apk add` needed for PostgreSQL connectivity.
- **Predis over phpredis** — The `php-nginx` base image does not include the `phpredis` C extension. Use the `predis/predis` Composer package and set `REDIS_CLIENT=predis` to avoid "class Redis not found" errors.
- **Object storage requires path-style** — Zerops object storage uses MinIO, which requires `AWS_USE_PATH_STYLE_ENDPOINT=true`. Without it, the SDK attempts virtual-hosted bucket URLs that MinIO cannot resolve.
- **Vite manifest missing on dev after fresh deploy** — the `dev` setup intentionally omits `npm run build` from `buildCommands` so the HMR workflow (`npm run dev` via SSH) stays fast. Any view rendering `@vite(...)` therefore 500s with `Vite manifest not found at: /var/www/public/build/manifest.json` on the first request after a `zerops_deploy`. Fix: run `ssh appdev 'cd /var/www && npm run build'` once after the deploy and before `zerops_verify` — SSHFS propagates the manifest into the container without a redeploy. For iterative work, `ssh appdev 'cd /var/www && nohup npm run dev > /tmp/vite.log 2>&1 &'` drops `public/build/hot` and Laravel routes asset URLs to the dev server. **Do NOT add `npm run build` to dev `buildCommands`** — it adds ~20–30 s to every `zcli push` and defeats the HMR-first design.

---

## Related Recipes

- [Laravel minimal](https://app.zerops.io/recipes/laravel-minimal.md)
- [PHP Hello world](https://app.zerops.io/recipes/php-hello-world.md)
- [Bun Hello World](https://app.zerops.io/recipes/bun-hello-world.md)
- [Go Hello World](https://app.zerops.io/recipes/go-hello-world.md)

