Deploy a Laravel app
From a GitHub repository to a live Laravel app with zero settings: PHP version and extensions from your lock file, its database, cache and workers made for it.
Updated 27 Oct 20269 min read
A standard Laravel app deploys on Tanzahost with no changes and no settings. Before anything is built, we read your code and show you a plan: the PHP version, the extensions, the database, cache and email we'll make, the variables we'll fill, and the workers your code needs, each with why and with its price first. This page follows Asha, who puts the open-source laravel-blog online in her project blog, in location UK-01 · United Kingdom.
You need your Laravel app in a GitHub repository (or as a ZIP file, or a public repository's link), and a Tanzahost account with a card saved.
Prepare your repository
Commit composer.json and composer.lock. We know it's Laravel when the code has an artisan file and composer.json requires laravel/framework. That's all we need: nothing else has to change.
What we read, and what we choose
We read the files that say what your code needs, and run none of them:
The PHP version from every constraint at once:
require.phpincomposer.json,config.platform.php, and thephpevery package incomposer.lockaccepts. We use our default, PHP 8.4, when everything allows it, otherwise the newest version they all allow. laravel-blog asks for^8.3, but its lock holdsnette/schema, which stops at 8.4, so its plan says "PHP 8.4 (your packages accept 8.3 and 8.4)". If nothing we offer fits, the plan says which two packages disagree.PHP extensions your packages need, even when only the lock names them:
pcntlfor Horizon,exifandgdfor pictures,zipfor backups,rediswhen your queue or cache uses Redis through phpredis, and the database's own (pdo_mysqlorpdo_pgsql). Each line says which package needs it.Node.js for your assets, when
package.jsonhas abuildscript: the version in.node-version,.nvmrcorengines, or the nearest newer one we offer when it no longer gets security fixes (laravel-blog asks for 20 and gets 22), and the exact package manager inpackageManager, like Yarn 4.5.1.Your config files, as text: which variable names
config/*.phpreally reads, and what they fall back to. Laravel 11 readsCACHE_STORE, not the olderCACHE_DRIVERsome example files still have.The health address: Laravel 11 and later declare
/up, which answers without the database, so a new version isn't held back because its home page needs data that isn't there yet.
Build and deploy files your code carries for other hosts are read as hints only (the PHP version, extensions and services they name), never run.
To choose something else, change it on the plan, in the app's Set up and run settings, or in the settings file: your choice wins. If your packages don't accept a version you picked, the plan says so before the build and offers the one they accept.
If your code needs an extension, a version or a service we don't offer, the plan says so in one sentence before anything is built, with Ask us to add it. You never wait ten minutes for a build that can't work.
Your .env file holds passwords. Leave it out of Git: its settings become encrypted variables, below.
Create a project and add the app
In the dashboard, choose New project, give it a name, and pick its location: the one closest to your visitors.
In the project, choose Add a service, then App or website.
Name the app, then choose GitHub and pick your repository and branch, or paste a public repository's link.
The page reads your code and shows its plan. For laravel-blog it says:
Laravel 11 on PHP 8.4, with Node.js 22 and Yarn 4.5.1 for the assets;
extensions added for your code: pcntl (Horizon), exif and gd (pictures), zip (backups), redis (your queue);
a MySQL database, a cache for your queue, and email from your project, each with its price a month first;
Horizon as a worker, with its price;
Needs your key: Pusher (live comments), which stays off until you add it.
Make the database, cache and email for me is on. Turn it off if you'd rather add them later. A new account starts on the shared MySQL, which costs least; you can choose A database server of its own instead.
Set its limits: the most memory and processor time it may use. They cost nothing by themselves.
Choose Deploy. The button says Making the database… first: the database and cache are made and connected before your app's first version starts.
You watch the build's log live. See Deploying.
A new Laravel app's .env.example has DB_CONNECTION=sqlite. A file database would be lost here at every deploy and can't be backed up or shared by two copies, so we make MySQL for it instead: Laravel works with it as it is. If you really want the file, choose Keep SQLite (one copy only) on the plan: the file then lives in a folder kept between deploys, and the app runs as one copy.
How your app is served
Your app runs on the same web server and PHP as our sites, with public/ as its document root, several requests at a time, and your .htaccess working. A start command you set in Set up and run still wins.
Your variables, made for you
Open your app's Variables tab. Values are stored encrypted, shown masked, and given to your app when it starts. Before your first deploy we fill them from your code's .env.example, by kind:
| Kind | Example | What we do |
|---|---|---|
| Made | APP_KEY | A new key, base64: and 32 random bytes, kept encrypted |
| Filled | APP_URL, APP_ENV=production, APP_DEBUG=false, LOG_CHANNEL=stderr | Your app's address (changed when it gets your domain), and production values |
| Copied | SESSION_DRIVER=file, SESSION_LIFETIME=120, APP_NAME | As your example writes them |
| From a service | DB_*, REDIS_*, MAIL_* | From the database, cache and email made for the app |
| Needs your key | PUSHER_*, AWS_*, STRIPE_* | Left empty and listed on the plan; BROADCAST_DRIVER stays on log until you add them |
We never copy a password, a token or an address on your own computer (localhost, 127.0.0.1, *.test) from the example, and we never replace a variable you set. A setting whose fallback points at a table your migrations don't make, like CACHE_STORE=database without a cache table, gets a working value: your cache, or file in the kept storage folder.
Email goes out from your project's email domain. If the project has none set up yet, messages go to the app's log and the plan says so, with Set up email.
After you change a variable, the page says Redeploy to apply until a deploy uses it.
Before going live: migrations and the rest
When we make the database for your app, Run migrations on each deploy is on, in the app's Laravel card; you can turn it off there. On every deploy, these run once, in their own sandbox, after the build and before visitors switch to the new version, each shown in the log:
php artisan migrate --force
php artisan optimizeA command of your own in Before going live runs after these.
The link storage:link makes, from public/storage to your kept storage folder, is made by the build, so uploaded pictures show from the first deploy.
artisan optimize is skipped, with a note, when code outside config/ calls env(), because Laravel's cached settings would hide those values.
The deploy stops, and the version before it keeps running. The deployment shows the command's last lines, so you can see why.
The same card has Run migrations, Storage link, Clear cache, Maintenance on and Maintenance off.
Sample data, when you ask
If your code has a database seeder, the app's page offers Add the sample data once, after the first deploy. Nothing is seeded by itself. When the seeder writes a login everyone can read in the code, we say so first: for laravel-blog, "This creates the login demo@demo.com with the password demodemo. Change it straight after."
Workers and the scheduler
When your code needs a queue worker, the plan adds one, on by default with its price shown: php artisan horizon when you use Horizon, otherwise php artisan queue:work --tries=3. When routes/console.php or withSchedule schedules tasks, the plan adds Laravel's scheduler every minute:
php artisan schedule:runA Laravel 11 app that still keeps its tasks in app/Console/Kernel.php gets a note instead: Laravel 11 doesn't load that file. If your app queues jobs and nothing runs them, its page says so. Workers are charged like an app, for what they use. See Workers and Scheduled tasks.
If a build fails
We read why, fix what we safely can, and try again by ourselves, at most twice: a missing PHP extension from our list, a PHP version your lock rejects, a build that ran out of memory. The next build's log starts with what we did, like "We added the PHP extension gd and tried again". If it still fails, the build's page says What went wrong, What we did and What you can do, with a Fix it button for anything that needs your yes, and Ask Sali beside it.
Connect your domain
Your app is already live on its free address, like blog-7h3k.tanzahost.co. On the app's Domains tab, choose Add your domain: use our nameservers, connect Cloudflare, or add the records we show where your domain is registered.
We check every minute. When the records are right, SSL is issued by itself, and the free address sends visitors to your domain. See Add a domain.
Every push to your branch now deploys without downtime. If a new version stops by itself within five minutes, visitors go back to the previous one by themselves.
Optional: the settings file
For anything special, add tanzahost.json to the root of your repository. It can choose the PHP version, extensions and PHP settings, the health address, and the folders to keep between deploys. Choices made in the dashboard win over the file.
{
"version": "8.4",
"health": "/up",
"php": {
"extensions": ["intl", "gd"],
"ini": { "upload_max_filesize": "64M" }
}
}Laravel's storage is already kept between deploys. Every field is on The settings file.