Dashboard

Build and deploy

Laravel

What HCode does when it builds and starts a Laravel app, and how to set APP_KEY, the database, migrations, sessions and scheduled commands.

HCode builds a Laravel app without a Dockerfile. It installs your PHP and Node.js packages, builds the Vite assets, and when the app starts it runs your migrations before it serves requests. You set a few environment variables before the first deploy.

How HCode recognises Laravel

A repository is a Laravel app when composer.json requires laravel/framework, or when an artisan file sits next to composer.json or index.php. HCode checks for Laravel before package.json, so the Vite package.json that comes with every new Laravel app doesn't make it look like a static site. Only a Dockerfile is checked earlier; see which check wins.

The startup steps below, and serving the public folder, depend on the artisan file at the root of the app. A Laravel app without one is built and served like plain PHP.

The plan suggests a web app on port 3000 with no start command. Leave Start command empty. The app's own startup runs the migrations and the web server, and a start command you set replaces all of it.

What the build does

  1. Installs PHP. The version comes from require.php in composer.json, so ^8.3 gives PHP 8.3. Without one, it's PHP 8.4.
  2. Installs PHP extensions: the ones Laravel needs, every ext-* entry in composer.json's require, the database driver for DB_CONNECTION (pgsql or mysql), and Redis when REDIS_URL or REDIS_HOST is set or a Composer package name contains redis.
  3. Runs composer install --optimize-autoloader --no-scripts --no-interaction. Your Composer scripts don't run during the build.
  4. If there is a package.json, installs your Node.js packages and runs its build script. In a standard Laravel app that is vite build, so the compiled CSS and JavaScript ship with the app.
  5. If there is a package.json, also creates the storage and bootstrap/cache folders and runs php artisan config:cache, event:cache, route:cache and view:cache.

Your environment variables are available to the build, and some of them change what it installs. Set DB_CONNECTION, and REDIS_URL if you use Redis, before the first deploy. If you add or change one later, deploy again: Apply settings and Restart reuse the last build, which doesn't have the new driver. Variables starting with VITE_ are written into the assets, so they need a new build too (see When changes take effect).

Your database can't be reached during the build, only once the app runs. See Databases during the build.

What happens when the app starts

Each time the app starts, including after a deploy or a restart, it runs:

bash
php artisan migrate --force
php artisan storage:link
php artisan optimize:clear
php artisan optimize

Then FrankenPHP serves the public folder on the port in PORT, which is 3000 unless you change Runtime port. If any of these commands fails, the app stops before it serves anything and the deploy fails. The runtime logs show the error.

php artisan optimize caches the configuration again from the app's current environment variables, so a changed variable takes effect on the next restart.

A deploy starts the new release before it stops the old one, so the new release's migrations run while the previous release is still serving. A migration that drops or renames a column the old code uses can break it while both are running. Add the new column in one deploy and remove the old one in a later one.

A Start command of your own, or a web: line in a Procfile, replaces this startup. None of the four commands run, and your command has to start the web server itself.

Skip the migrations

Add this environment variable, then restart the app or deploy again:

text
RAILPACK_SKIP_MIGRATIONS=true

The value must be exactly true. The other three commands still run. Run your migrations another way, for example from your own machine with external access to the database.

Environment variables to set

Add these under Settings → Environment variables, or in Environment variables when you create the project. See Environment variables.

Variable Value
APP_KEY Generated for you when you create the project in the dashboard. Otherwise, the output of php artisan key:generate --show
DB_CONNECTION pgsql for an HCode database
DB_URL The database's internal connection string

APP_KEY

Laravel encrypts sessions and cookies with APP_KEY and won't serve requests without one.

When you create the project in the dashboard from a GitHub repository and HCode recognises Laravel, HCode generates the key and stores it as a secret. The create form shows an APP_KEY row that says so. To use a key of your own, type it in that row.

In every other case you set the key yourself: a project created from an uploaded ZIP or through the API, and a project that already exists. Generate it on your own machine, in the app's folder:

bash
php artisan key:generate --show

It prints a value that starts with base64:. Add the whole value as APP_KEY and tick Secret.

Keep the same key from then on: a new key signs everyone out and leaves anything encrypted with the old key unreadable. A secret's value isn't shown again after it is saved, so you can't copy a generated key into another project. If two projects need the same key, such as the scheduler below, generate one yourself and set it in both.

Without it, the deploy fails and "What went wrong" says "Laravel needs an APP_KEY."

The database

HCode databases are PostgreSQL. Create one on the project's Databases tab (see Databases overview), copy its internal connection string, and add:

text
DB_CONNECTION=pgsql
DB_URL=postgres://u1a2b3c4d:dbp_xxxxxxxx@db-0a1b2c3d4e5f:5432/db_my-app?sslmode=disable

Use the string from your own database card, and mark DB_URL as Secret. If you'd rather use separate variables, set DB_HOST, DB_PORT (5432), DB_DATABASE, DB_USERNAME and DB_PASSWORD from the parts of the string.

DB_CONNECTION matters twice. The build reads it to install the PHP driver, and a new Laravel app uses SQLite when it isn't set. An SQLite file lives inside the app's container, and each deploy or restart starts a fresh one, so the data doesn't last.

For a MySQL database hosted somewhere else, DB_CONNECTION=mysql installs the MySQL driver instead.

Sessions and cache

A new Laravel app keeps sessions and cache in the database, in the sessions and cache tables its migrations create. The migrations at startup create them on the first deploy. You have three choices:

Setting What you need What happens on a deploy or restart
Database (the default) A database and the migrations Sessions and cache are kept
SESSION_DRIVER=file and CACHE_STORE=file Nothing Each deploy or restart starts a fresh copy of the app, so the files are gone and everyone is signed out
SESSION_DRIVER=redis and CACHE_STORE=redis An HCode Redis database, with its connection string in REDIS_URL Sessions and cache are kept

File sessions suit an app with no database. Setting REDIS_URL before the build also installs PHP's Redis extension.

Scheduled commands

HCode can't run commands on a schedule yet. Scheduled commands for a project are planned. Until then, run Laravel's scheduler as a second project, a background worker that keeps php artisan schedule:work running. It checks your schedule every minute and runs the commands that are due.

  1. Create a new project from the same repository and branch.
  2. Under App type, choose Background worker.
  3. Set Start command to php artisan schedule:work.
  4. In Optional settings, under Environment variables, add the same variables as your web app. Type the web app's key in the APP_KEY row: left empty, HCode generates a different key for this project, and the scheduler can't read anything the web app encrypted.
  5. Give it its own database connection (below), then create and deploy it.

The database's internal connection string only works inside its own project, so the scheduler can't use it. Turn on external access for the web app's database, create a token for the scheduler, and set DB_URL in the scheduler to the connection string the token comes with. That string ends in sslmode=require, which external connections need. When the token expires the scheduler loses its database, so choose Never under Expires in, or replace the token before it runs out.

Redis has no external access, so the scheduler can't reach the web app's Redis database. If your web app keeps its cache in Redis, set CACHE_STORE=database in the scheduler.

The scheduler's start command replaces the startup steps, so it doesn't run migrations or php artisan optimize; the web app does that. When the repository has a package.json, the build caches the configuration, so after changing one of the scheduler's variables, deploy it again rather than restarting it. It counts toward your plan like any other project.

A queue worker works the same way, with php artisan queue:work as its start command.