Deploy Laravel on Temps

Choose the Autopack (PHP) preset (nixpacks-php) and run up: Temps installs your Composer dependencies and serves public/ with FrankenPHP on $PORT, behind automatic HTTPS. Link managed PostgreSQL and read POSTGRES_URL, and run php artisan migrate yourself after a deploy that changes the schema.

This guide describes how the nightly release channel builds your app, using the Autopack builder. Beta (v0.1.0-beta.56) also builds with Autopack, but some details below may differ. Earlier releases and the stable channel build with Nixpacks and behave differently. To run nightly, see Release channels.

For an overview of self-hosting Laravel, including costs, see Deploy Laravel to your own server. Every framework and the preset it needs is listed under Supported platforms.


Quickstart

Temps doesn't detect PHP projects: neither composer.json nor artisan is a detection signal, and deploying without a preset stops with "Could not auto-detect project type from files". Choose the Autopack (PHP) preset (nixpacks-php):

npx @temps-sdk/cli up --preset nixpacks-php

You can also set it under Project → Settings → Build & deploy → Build → Framework.

The build runs composer install --no-dev and composer dump-autoload --optimize, then starts FrankenPHP, a single process that is both the web server and the PHP runtime. It serves public/, so the repository root (and your .env) is never exposed. The PHP version is the first one in the require.php constraint in composer.json (^8.2 gives 8.2), defaulting to 8.3 when there is none. A web: line in a Procfile replaces the start command.

On beta.56 the build doesn't compile front-end assets, and a page that uses @vite returns 500 without public/build/manifest.json: run npm run build locally, take /public/build out of .gitignore and commit the folder, or build the assets in your own Dockerfile. Nightly builds, and the next release, run your build script for you, pick the newest PHP version that composer.json and composer.lock both allow, and set LOG_CHANNEL=stderr.

A new Laravel app needs three things before its first deploy passes the health check:

  1. An APP_KEY. Without it every page returns 500.
  2. A PostgreSQL database. A new app uses SQLite, and the database file doesn't exist in the container, so pages that use sessions or the cache return 500.
  3. A health check on /up, so the first deploy passes before you've run migrations.

What Temps handles automatically

FeatureHow Temps handles it
Installcomposer install --no-dev, then composer dump-autoload --optimize
AssetsNot built on beta.56: commit public/build. Nightly builds, and the next release, install your Node.js dependencies and run your build script (vite build)
PHP versionThe first version in the require.php constraint in composer.json, defaulting to 8.3. From the next release: the newest version composer.json and composer.lock both allow
PHP extensionsBuilt from the ext-* entries in composer.json require
Web serverFrankenPHP serving public/
EnvironmentAPP_ENV=production. From the next release, also LOG_CHANNEL=stderr, so Laravel's logs appear in your container logs; on beta.56, set it as a project variable
HTTPSLet's Encrypt, auto-renewed
PortListens on the PORT Temps injects
Health checksHTTP health check on /

What it doesn't do:

  • Config and route caching. php artisan config:cache and route:cache aren't run. Laravel works without them.
  • Migrations. Database migrations are not run automatically. See Run migrations below.

Run migrations

Temps does not have a remote "run one command" CLI action yet, so migrations are run directly against the container over SSH. Find the container name, then run the migration:

bunx @temps-sdk/cli projects show -p my-laravel-app --json   # note the numeric "id"
bunx @temps-sdk/cli containers list -p <project-id>          # note the container_name
ssh your-server 'docker exec <container_name> php artisan migrate --force'

Run this manually (or from your CI/CD pipeline right after the deploy step) every time a deploy includes a schema change.

On the first deploy the tables don't exist yet. Laravel stores sessions, the cache and queued jobs in the database by default, so / returns 500 until the migrations have run, and the deploy would fail its health check before you could migrate. Point the health check at Laravel's built-in /up route (Laravel 11 and later), which doesn't use the database, with health.path in a .temps.yaml at the repository root (see Health checks):

.temps.yaml

health:
  path: /up

Do not add php artisan migrate --force to your Dockerfile's CMD, start command, or Procfile web: line. With rolling deploys, restarts or multiple replicas, that command can run more than once, and concurrent migration runs against the same database can conflict or corrupt data.


Environment variables

npx @temps-sdk/cli environments vars set APP_KEY "$(php artisan key:generate --show)" -e production

npx @temps-sdk/cli environments vars set APP_URL "https://yourdomain.com" -e production

Without APP_KEY every page returns 500. Changing it later signs users out and makes previously encrypted values unreadable, so set it once.

Temps terminates HTTPS at its proxy and forwards X-Forwarded-Proto. Trust the proxy so Laravel generates https:// URLs; in Laravel 11 and later, call $middleware->trustProxies(at: '*') in bootstrap/app.php.


Managed services

Create PostgreSQL and Redis under Databases → Create database, then link them from your project's Databases page. Temps injects POSTGRES_URL (plus POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER and POSTGRES_PASSWORD) and REDIS_URL. It doesn't inject DATABASE_URL or DB_URL, and it doesn't expand references such as ${POSTGRES_URL} inside other variables, so point the pgsql connection at POSTGRES_URL in config/database.php:

// config/database.php
'pgsql' => [
    'driver' => 'pgsql',
    'url' => env('POSTGRES_URL'),
    // ...
],

Then set DB_CONNECTION=pgsql, and declare the PHP extension Laravel needs to reach PostgreSQL:

composer require ext-pdo_pgsql:*

Without ext-pdo_pgsql in your composer.json, pages that touch the database fail with "could not find driver".

A linked Redis service injects REDIS_URL, REDIS_HOST, REDIS_PORT and REDIS_PASSWORD; Laravel's Redis configuration already reads REDIS_URL. Laravel's default Redis client, phpredis, needs ext-redis in your composer.json too. See Deploy with a Database.


Queues, Horizon and the scheduler

Temps runs one web process per project. In a Procfile, only the web: line is used, as the start command; worker:, scheduler: and horizon: lines are ignored, so Temps doesn't run queue:work, horizon or schedule:work for you. A project that doesn't answer HTTP also fails its health check, so a worker can't run as a project of its own.

  • Scheduler: call the scheduler over HTTP with a cron job in .temps.yaml. Add a route that checks the CRON_SECRET bearer token and runs Artisan::call('schedule:run'), and schedule it every minute.
  • Queue workers and Horizon: deploy the app as a Docker Compose project with a web service and a worker service that runs php artisan queue:work (or php artisan horizon). Services that don't publish a port stay off the proxy. Set QUEUE_CONNECTION=redis and link a Redis service.
  • Light workloads: QUEUE_CONNECTION=sync runs jobs inline during the request, with no worker. With Laravel's default QUEUE_CONNECTION=database and no worker, dispatched jobs just wait in the jobs table.

Platform behavior

These rules apply to every app deployed on Temps, regardless of framework.

The one requirement: your app must listen on the port in the PORT environment variable and bind to 0.0.0.0 — not localhost or 127.0.0.1. Temps runs your app in a container and routes traffic from the host, so an app bound to localhost only accepts connections from inside the container and will fail its health check.

Health checks

After your container starts, Temps sends HTTP GET requests to verify it is healthy before routing traffic to it.

  • Path: / (the root of your application) by default
  • Success: 2 consecutive responses with a 2xx, 3xx, 404 or 405 status code
  • Timeout: 300 seconds (5 minutes) for the app to become healthy, fixed in v0.1.0-beta.56. If the app answers with error responses (other 4xx, or 5xx) for 60 seconds in a row, the deployment fails sooner. From the next release the timeout is configurable from 30 to 3600 seconds under Project → Settings → Build & deploy
  • Retry interval: 2 seconds between successful checks, 5 seconds after an error response or a failed connection

Connection errors and other 4xx or 5xx responses are retried until the timeout; if the app hasn't passed by then, the deployment fails. The check runs once, at deploy time. To check a different path, add a .temps.yaml to your repository root:

.temps.yaml
health:
  path: /health

Only path is used. Other health fields (status, interval, timeout, retries) are accepted but currently have no effect.

Add a dedicated /health endpoint that returns a simple 200. This avoids issues where / requires authentication or returns a redirect.

Auto-injected environment variables

Temps injects these variables into every deployment automatically:

VariableValueDescription
PORTResolved portThe port your app must listen on
HOST0.0.0.0Bind address
SENTRY_DSNAuto-generatedError tracking endpoint
TEMPS_API_URLYour Temps URLPlatform API endpoint
TEMPS_API_TOKENDeployment tokenAuthentication for Temps SDKs
OTEL_EXPORTER_OTLP_ENDPOINTYour Temps OTLP URLOpenTelemetry trace collection
OTEL_SERVICE_NAMEProject nameService identifier for traces

You do not need to configure these manually. They are available in process.env (Node.js), os.environ (Python), os.Getenv (Go), and the equivalent in other languages.

Deploying another server-side framework with a database? The Rails and Django guides follow the same pattern.


Next Steps

Last updated

Was this page helpful?