Laravel and Ubuntu work well together, and the 26.04 LTS release makes the setup shorter than it used to be. PHP 8.5 comes from the default repositories, so the old habit of adding a third-party PPA is gone. Node.js and MariaDB are in there too.
This tutorial takes you from a fresh Ubuntu 26.04 install to a working Laravel 13 application served by Nginx and PHP-FPM, backed by MariaDB. You will also build a small feature at the end to see how the pieces fit. Run the commands in order. The errors people hit most often are covered near the bottom.
Table of Contents
- Prerequisites
- Step 1: Update Ubuntu and install base packages
- Step 2: Install PHP 8.5 and the required extensions
- Step 3: Install Composer
- Step 4: Install Node.js and npm
- Step 5: Install and prepare the database
- Step 6: Install the Laravel installer
- Step 7: Create your Laravel project
- Step 8: Configure the .env file
- Step 9: Run the development server
- Step 10: Set file permissions
- Step 11: Serve Laravel with Nginx and PHP-FPM
- Step 12: Add HTTPS and a firewall
- Configure Laravel for production
- Using Laravel: build your first feature
- Troubleshooting common errors
- FAQ
Prerequisites
Before you start, make sure you have the following:
- Ubuntu 26.04 LTS (Resolute Raccoon), desktop or server. A VPS, a virtual machine, and a spare laptop all work.
- A user with sudo access. Do not work as root. It causes more permission trouble than it saves.
- At least 2 GB of RAM. Composer can get killed on 1 GB machines. A swap file fixes that (see troubleshooting).
- An internet connection for packages and Composer dependencies.
- Basic terminal skills and a text editor you can use over SSH, such as
nano. - A domain name pointed at your server, only if you want HTTPS in Step 12.
Here is the stack this tutorial builds:
| Component | Version | Notes |
|---|---|---|
| Ubuntu | 26.04 LTS | Five years of standard security updates, through April 2031 |
| Laravel | 13.x | Requires PHP 8.3 or newer |
| PHP | 8.5 | In the default Ubuntu repositories |
| Composer | 2.x | Installed from getcomposer.org |
| Node.js | 22.x | Needed to build front-end assets with Vite |
| MariaDB | 11.8 | In the default Ubuntu repositories |
| Nginx | Ubuntu package | Paired with PHP-FPM |
Step 1: Update Ubuntu and Install Base Packages
Refresh the package index, apply pending updates, and install the small tools the rest of the tutorial depends on.
sudo apt update
sudo apt upgrade -y
sudo apt install -y curl git unzip acl ca-certificates
unzip matters more than it looks. Without it, Composer falls back to slower git clones and prints warnings. acl is used in Step 10 for clean permissions.
Confirm you are on the right release:
lsb_release -d
The output should read Ubuntu 26.04 LTS.
Step 2: Install PHP 8.5 and the Required Extensions
Ubuntu 26.04 ships PHP 8.5 in its own repositories. You do not need Ondřej Surý’s PPA, and you should not add one. Install the CLI, FPM, and the extensions Laravel and most packages expect:
sudo apt install -y php8.5-cli php8.5-fpm php8.5-common php8.5-mbstring php8.5-xml php8.5-curl php8.5-zip php8.5-bcmath php8.5-intl php8.5-gd php8.5-mysql php8.5-sqlite3
What each package is for:
- php8.5-cli runs
artisanand Composer from the terminal. - php8.5-fpm is the process manager Nginx talks to.
- php8.5-common brings in the rest of the extensions Laravel requires, including Ctype, Fileinfo, Tokenizer, and PDO.
- php8.5-mbstring, php8.5-xml, php8.5-curl, php8.5-zip cover string handling, DOM and XML parsing, HTTP requests, and archive support.
- php8.5-bcmath and php8.5-intl are not strictly required, but many popular packages use them.
- php8.5-gd handles image processing.
- php8.5-mysql provides the PDO driver for MariaDB and MySQL.
- php8.5-sqlite3 provides SQLite, which a fresh Laravel app uses by default.
Verify the install:
php -v
php -m | grep -Ei 'mbstring|xml|curl|zip|pdo_mysql|sqlite'
You should see PHP 8.5.x (cli) and each extension listed.
Step 3: Install Composer
Composer is the PHP package manager, and Laravel cannot be installed without it. Ubuntu packages Composer too, but the official installer gives you the current release and lets you update it with composer self-update. This version checks the installer signature before running it:
cd ~
curl -sS https://getcomposer.org/installer -o composer-setup.php
HASH="$(curl -sS https://composer.github.io/installer.sig)"
php -r "if (hash_file('sha384', 'composer-setup.php') === '$HASH') { echo 'Installer verified' . PHP_EOL; } else { echo 'Installer corrupt' . PHP_EOL; unlink('composer-setup.php'); exit(1); }"
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php
composer --version
If the first line of output says Installer verified, you are good. Never run composer create-project or composer install with sudo. It leaves root-owned files in your project and creates permission problems later.
Step 4: Install Node.js and npm
Laravel uses Vite to compile CSS and JavaScript, so you need Node.js even if your app is mostly server-rendered. Ubuntu 26.04 packages Node.js 22, and npm is a separate package:
sudo apt install -y nodejs npm
node -v
npm -v
Recent Vite releases need Node 20.19 or 22.12 and newer. If node -v prints something older than that, install a newer release from NodeSource or with nvm instead.
Step 5: Install and Prepare the Database
You have two sensible options.
Option A: SQLite (nothing to install)
A new Laravel project uses SQLite out of the box, and you already installed the PHP driver in Step 2. This is fine for learning and small projects. Skip to Step 6.
Option B: MariaDB (recommended for real projects)
Install the server and run the hardening script:
sudo apt install -y mariadb-server
sudo systemctl enable --now mariadb
sudo mariadb-secure-installation
Accept the defaults that remove anonymous users, disallow remote root login, and drop the test database. The root account uses socket authentication, so you can log in with sudo mariadb and no password.
Now create a database and a dedicated user. Never point your application at the root account.
sudo mariadb
CREATE DATABASE blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'blog_user'@'localhost' IDENTIFIED BY 'use-a-long-random-password';
GRANT ALL PRIVILEGES ON blog.* TO 'blog_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Replace the password with something real. You will need it in Step 8.
Step 6: Install the Laravel Installer
You can create projects with plain Composer, but the Laravel installer is faster and walks you through the starter options. Install it globally:
composer global require laravel/installer
Add Composer’s global bin directory to your PATH so the laravel command is found:
echo 'export PATH="$HOME/.config/composer/vendor/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
laravel --version
If the command is still not found, print the real location with composer global config bin-dir --absolute and use that path instead.
Step 7: Create Your Laravel Project
This tutorial puts the project in /var/www/blog so Nginx can read it later. Do not use your home directory for this. Home folders on Ubuntu are private by default, and pointing Nginx at one produces confusing 403 errors.
Give your user ownership of /var/www so you never need sudo to create projects, then create the app:
sudo chown $USER:www-data /var/www
cd /var/www
laravel new blog
The installer asks a few questions. Answers that work for this tutorial:
- Starter kit: choose None. The React, Vue, and Livewire kits add ready-made login and registration pages. They are worth trying later, but they add noise to a first setup.
- Testing framework: Pest or PHPUnit. Either works. Pest is the default.
- Database: choose MariaDB if you followed Option B, or SQLite if you skipped it.
- Migrations: if you picked MariaDB and the installer offers to run them, say no. The credentials are not in
.envyet. - npm install and build: say yes.
- Newer installer versions may also ask about Laravel Boost, an AI-assisted coding add-on. Answer no unless you want it.
Prefer plain Composer? This does the same job without the prompts:
composer create-project laravel/laravel blog
Move into the project and check the version:
cd blog
php artisan --version
The output should read Laravel Framework 13.x.x. Here is what lives in the project folder:
- app/ holds your models, controllers, and application code.
- bootstrap/ boots the framework.
bootstrap/cachemust be writable. - config/ holds configuration files for the database, mail, queues, and more.
- database/ holds migrations, seeders, factories, and the SQLite file if you use it.
- public/ is the only folder the web server should expose. It contains
index.php. - resources/ holds Blade views, CSS, and JavaScript source files.
- routes/ defines your URLs.
routes/web.phpis where you will start. - storage/ holds logs, cache files, compiled views, and uploads. It must be writable.
- tests/ holds your automated tests.
- .env holds environment-specific settings and secrets. Never commit it.
Step 8: Configure the .env File
Open the environment file:
nano .env
Set these values to match your setup:
APP_NAME=Blog
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000
DB_CONNECTION=mariadb
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=blog_user
DB_PASSWORD=use-a-long-random-password
A few notes on what you are looking at:
APP_KEYis already filled in. Laravel generated it during creation. It encrypts sessions and cookies, so do not change it on a live site.APP_DEBUG=trueshows detailed error pages. That is useful locally and dangerous on a public server. It gets switched off later.- The connection name for MariaDB is
mariadb. Usemysqlif you installed MySQL instead. - If you stayed with SQLite, leave the
DB_*lines alone. Laravel usesdatabase/database.sqlite. - Sessions, cache, and queues use the database driver by default, so the default migrations must run before your app will load pages.
Run the migrations:
php artisan migrate
This creates the default tables. If the installer already ran migrations, the command reports that there is nothing to migrate. Either result is fine. For a summary of how Laravel sees your environment, run:
php artisan about
Step 9: Run the Development Server
The quickest way to see your app is the dev command that ships with the project:
composer run dev
It starts the PHP development server, a queue listener, a log viewer, and Vite together. Open http://localhost:8000 in a browser and you will see the Laravel welcome page. Press Ctrl+C to stop everything.
If you only need the web server, this is enough:
php artisan serve
Working on a remote server over SSH? The dev server listens on 127.0.0.1, which is good. Forward the ports from your own machine instead of opening them to the internet:
ssh -L 8000:127.0.0.1:8000 -L 5173:127.0.0.1:5173 your-user@your-server
Then browse to http://localhost:8000 on your own computer. The development server is for development only. Steps 10 to 12 set up the real thing.
Step 10: Set File Permissions
Laravel writes to storage and bootstrap/cache. PHP-FPM runs as www-data, while you run artisan commands as your own user. If only one of you can write, you get random 500 errors. Access control lists solve it cleanly:
cd /var/www/blog
sudo setfacl -R -m u:www-data:rwX -m u:$USER:rwX storage bootstrap/cache
sudo setfacl -dR -m u:www-data:rwX -m u:$USER:rwX storage bootstrap/cache
The second command sets default ACLs, so files created later inherit the same access. If you kept SQLite, run the same two commands against the database folder as well. SQLite writes temporary files next to the database file, so the folder needs to be writable, not just the file.
Step 11: Serve Laravel with Nginx and PHP-FPM
Install Nginx and make sure both services start on boot:
sudo apt install -y nginx
sudo systemctl enable --now nginx php8.5-fpm
Create a server block for the site:
sudo nano /etc/nginx/sites-available/blog
Paste this configuration. Change server_name to your domain, or to _ if you are testing by IP address.
server {
listen 80;
listen [::]:80;
server_name example.com;
root /var/www/blog/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
client_max_body_size 25M;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Two lines matter more than the rest. root must point to the public folder, never the project root, or visitors could download your .env file. The fastcgi_pass line must match the PHP-FPM socket, which includes the PHP version number.
Enable the site, remove the default one, test the syntax, and reload:
sudo ln -s /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/blog
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
Set APP_URL in .env to match your domain, then clear the cached config:
php artisan config:clear
Visit your domain or server IP. The Laravel welcome page should load, this time served by Nginx.
Step 12: Add HTTPS and a Firewall
If you have a domain pointing at the server, get a free certificate from Let’s Encrypt:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com
sudo certbot renew --dry-run
Certbot edits the Nginx config for you and sets up automatic renewal. The dry run confirms renewal will work. Afterwards, change APP_URL in .env to start with https://.
Ubuntu ships with the UFW firewall turned off. Turn it on, but allow SSH first or you will lock yourself out:
sudo ufw allow OpenSSH
sudo ufw allow "Nginx Full"
sudo ufw enable
sudo ufw status
Configure Laravel for Production
The setup so far is a working development box. Before real traffic arrives, change these things.
Environment and caching
In .env, set APP_ENV=production and APP_DEBUG=false. Then build optimized assets and cache the framework’s bootstrap files:
composer install --no-dev --optimize-autoloader
npm install
npm run build
php artisan storage:link
php artisan optimize
storage:link exposes storage/app/public through public/storage so uploaded files can be served. optimize caches config, routes, events, and views. After you change .env or any config file on a cached app, run php artisan optimize:clear and then php artisan optimize again.
PHP limits
Ubuntu keeps separate php.ini files for the CLI and for FPM. The web app uses the FPM one:
sudo nano /etc/php/8.5/fpm/php.ini
Typical values to raise for uploads and heavier requests:
memory_limit = 256M
upload_max_filesize = 20M
post_max_size = 25M
max_execution_time = 60
Keep post_max_size at or above upload_max_filesize, and keep Nginx’s client_max_body_size at or above both. Apply the change:
sudo systemctl restart php8.5-fpm
The scheduler
Laravel’s task scheduler needs a single cron entry that runs every minute. Add it for the www-data user:
sudo crontab -u www-data -e
* * * * * cd /var/www/blog && php artisan schedule:run >> /dev/null 2>&1
A queue worker that survives reboots
If your app sends mail or runs background jobs, run the queue worker as a systemd service:
sudo nano /etc/systemd/system/laravel-queue.service
[Unit]
Description=Laravel queue worker for blog
After=network.target mariadb.service
[Service]
User=www-data
Group=www-data
Restart=always
RestartSec=3
WorkingDirectory=/var/www/blog
ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3 --max-time=3600
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now laravel-queue
Queue workers keep your code in memory. After every deployment, run php artisan queue:restart so they pick up the new version.
Using Laravel: Build Your First Feature
A running welcome page proves the stack works. This section builds a tiny posts feature so you can see routes, controllers, models, migrations, and views working together.
Artisan commands worth knowing
Artisan is Laravel’s command-line tool. You will use it constantly:
php artisan listshows every available command.php artisan route:listshows every route in your app.php artisan make:model Post -mcreates a model and its migration.php artisan make:controller PostControllercreates a controller.php artisan migrateruns pending migrations.php artisan migrate:rollbackundoes the last batch.php artisan tinkeropens an interactive shell where you can run PHP against your app.php artisan testruns your test suite.php artisan optimize:clearwipes cached config, routes, and views when something behaves strangely.
Create the model and migration
cd /var/www/blog
php artisan make:model Post -m
Open the new file in database/migrations (its name ends in _create_posts_table.php) and define the columns inside up():
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});
In app/Models/Post.php, allow those two fields to be filled in bulk by adding this property to the class:
protected $fillable = ['title', 'body'];
Run the migration:
php artisan migrate
Create the controller
php artisan make:controller PostController
Replace the contents of app/Http/Controllers/PostController.php with:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
class PostController extends Controller
{
public function index()
{
return view('posts.index', [
'posts' => Post::latest()->get(),
]);
}
}
Add the route
In routes/web.php, add the import at the top of the file and the route below the existing one:
use App\Http\Controllers\PostController;
Route::get('/posts', [PostController::class, 'index']);
Create the Blade view
Create the file resources/views/posts/index.blade.php:
<h1>Posts</h1>
@forelse ($posts as $post)
<article>
<h2>{{ $post->title }}</h2>
<p>{{ $post->body }}</p>
</article>
@empty
<p>No posts yet.</p>
@endforelse
The double curly braces escape output automatically, which protects you from cross-site scripting by default. Visit /posts now and you will see the empty-state message.
Add data with Tinker
php artisan tinker
App\Models\Post::create(['title' => 'Hello from Ubuntu 26.04', 'body' => 'Laravel is running.']);
exit
Reload /posts. Your post appears. That is the full request cycle: Nginx hands the request to PHP-FPM, Laravel matches the route, the controller asks the model for data, MariaDB returns it, and Blade renders the HTML.
Where to go next
- Authentication: create a new project with one of the official starter kits for login, registration, and password reset.
- Validation and forms: add a
storemethod and use$request->validate(). - Relationships: link posts to users with Eloquent relationships.
- Testing: write feature tests in
tests/Featureand run them withphp artisan test. - Docker option: Laravel Sail runs the same kind of stack in containers if you prefer not to install services on the host.
Troubleshooting Common Errors
502 Bad Gateway
Nginx cannot reach PHP-FPM. Check that the service is running with sudo systemctl status php8.5-fpm. Then list /run/php/ and confirm the socket name matches the fastcgi_pass line in your Nginx config.
Only the homepage works, or you see the default Nginx page
Check that root ends in /public, that the try_files line inside location / is intact, that your site is symlinked into sites-enabled, and that the default site is removed. Run sudo nginx -t after any change.
500 error or a blank page
The real message is in the logs. Run tail -n 50 storage/logs/laravel.log and sudo tail -n 50 /var/log/nginx/error.log. The usual causes are a missing .env, a missing app key, or permission problems.
Permission denied on storage/logs/laravel.log
Re-run the two setfacl commands from Step 10. This typically happens when a file was created by one user before the ACLs existed. Fix an already-created log with sudo chmod 664 storage/logs/laravel.log.
“could not find driver”
The PHP database extension is missing. Install php8.5-mysql for MariaDB or MySQL, or php8.5-sqlite3 for SQLite, and then restart PHP-FPM with sudo systemctl restart php8.5-fpm.
“Access denied for user” or changes to .env are ignored
Test the credentials directly with mariadb -u blog_user -p blog. If that works, the problem is a cached config. Run php artisan config:clear.
“Vite manifest not found”
The front-end assets were never built. Run npm install and npm run build, or use composer run dev while developing.
“No application encryption key has been specified”
Run php artisan key:generate, then php artisan config:clear.
Composer is “Killed” or runs out of memory
Small servers run out of RAM during dependency installs. Add a 2 GB swap file:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
Composer says your PHP version does not satisfy the requirements
Laravel 13 needs PHP 8.3 or newer. Run php -v. If you see an older version, another PHP install is first in your PATH or you are on an older Ubuntu release.
FAQ
Which PHP version should I use with Laravel 13 on Ubuntu 26.04?
Use PHP 8.5, which is what Ubuntu 26.04 installs by default. Laravel 13 supports PHP 8.3 through 8.5, so the default package is inside the supported range and gets security updates through Ubuntu.
Can I use Apache instead of Nginx?
Yes. Install Apache with PHP-FPM or libapache2-mod-php8.5, set the DocumentRoot to /var/www/blog/public, enable mod_rewrite, and allow .htaccess overrides. Laravel ships with an .htaccess file in public that handles the routing.
Can I use MySQL or PostgreSQL instead of MariaDB?
Yes. Laravel supports both. Install the matching PHP extension (php8.5-mysql or php8.5-pgsql), set DB_CONNECTION to mysql or pgsql, and adjust the host, port, and credentials in .env.
Do these steps work on Ubuntu 24.04?
Mostly. Ubuntu 24.04 ships PHP 8.3, which meets the Laravel 13 minimum. Replace php8.5 with php8.3 in package names and paths, including the FPM socket in the Nginx config. Node.js and MariaDB versions will differ.
Do I really need Node.js?
If your app uses Vite, Tailwind, or any starter kit front end, yes. An API-only app with no front-end build can skip it.
How do I update a Laravel app after deploying changes?
Pull the new code, then run composer install --no-dev --optimize-autoloader, npm install, npm run build, php artisan migrate --force, php artisan optimize, and php artisan queue:restart. The --force flag is required to run migrations when APP_ENV is production.
Wrapping Up
You now have a clean Ubuntu 26.04 box running PHP 8.5, Composer, Node.js, MariaDB, and Nginx, with a Laravel 13 app behind it and a working first feature. The official Laravel documentation is the best next stop for routing, Eloquent, queues, and testing. For OS-level upkeep, sudo apt update && sudo apt upgrade keeps PHP, Nginx, and MariaDB patched through Ubuntu’s normal security channel.