Deploying a Laravel application

(This is part of a larger guide to deploying a Laravel and Vue.js web application.)

This article covers deploying a Laravel application.

Before beginning, you should have performed the application-specific server setup covered in the previous phase of the tutorial.

Background: the deployment directory structure used in this tutorial

This tutorial will use a deployment workflow called zero-downtime redeployment. We’ll cover this in detail when automating redeployment in the next phase of the tutorial, but we have to set up a directory structure supporting zero-downtime redeployment now.

If you want to read an explanation of this directory structure, open the details/summary elements below. Feel free to skip if impatient.

Explanation of directory structure used for deployment

For orientation, the directory structure after working through this section should look something like this:

/srv/www/laravel/
├── releases/
│   └── initial/
│       ├── app/
│       ├── bootstrap/
│       ├── config/
│       ├── ...
│       ├── .env -> ../../shared/.env
│       └── storage/ -> ../../shared/storage/
├── active -> releases/initial/  # symlink to initial release
└── shared/
    ├── .env
    └── storage/
  • The releases/ directory holds releases of your app.

  • active/ is a symlink pointing to the currently active release (i.e. the release served to the public Web). When you update your app with a new release, you’ll update the active symlink to point to the new release directory. We’ll cover this more thoroughly when covering automated zero-downtime redeployment in the last phase of the tutorial.

  • The shared/ directory contains your app’s .env file, storage/ directory, database.sqlite file if using SQLite, and any other files that are shared by all releases and/or not normally tracked by Git.

    This structure keeps these shared files decoupled from individual releases of your app, letting you, for example, keep using the same database and environment file when you update your app’s code.

    It also makes practical sense—remember that the .env and database.sqlite files (and often the storage directory, too) are Git-ignored in typical Laravel projects, and thus would not be uploaded when you git push your code to your server.

This directory structure makes redeployments easy—you upload the new release’s code into the releases directory, link shared files into place, and update active to point to the new release.

We’ll go through this deployment process manually in this article, then automate it in the next phase of the tutorial.

A few words on sharing the storage/ directory

In this guide I’ve decided to share the storage/ directory between releases of your app. This seems to be preferred setup for Laravel apps using zero-downtime redeployment, but there is still some confusion online about the best way to manage the storage/ directory between deployments.

The great benefit to sharing storage/ is that all user uploads, logs, cache files, and other files generated by your application (which should all be in storage/) will persist between redeployments.

The downside (which really isn’t that a big deal) is the need to manually remove the boilerplate release-specific storage/ directory during redeployments (or just entirely Git-ignore the storage/ directory, so you’re not even pushing it from your dev machine to your server).

Feel free to modify the instructions in this guide if you have a different, preferred way of handling the storage/ directory between deployments.

SSH Host entries on your development machine

In ~/.ssh/config on your development machine, create two new SSH Host entries, one to access your server over SSH, and one to push code to your server over SSH using Git:

# This entry is for SSH access to your server
Host laravel
  # This should be your server's IP address
  HostName 1.2.3.4
  # This is the user you'll later create to administer your web app
  User laravel
  # This should be the full path to the private SSH key you'll use to access your server
  IdentityFile ~/.ssh/LaravelApp_id_ed25519
# This entry is to push code to the server over SSH using Git
Host laravel_git
  # This should be your server's IP address
  HostName 1.2.3.4
  # This should be the full path to the private SSH key you'll use to access your server
  IdentityFile ~/.ssh/LaravelApp_id_ed25519
  # Leave this as is
  IdentitiesOnly yes

The entries have separate purposes:

  • With the first entry, you can access your server with ssh laravel instead of having to type ssh [email protected] -i ~/.ssh/LaravelApp_id_ed25519.
  • The second entry will be used to push code to your server when deploying.

It’s fine that both entries have most of their lines in common.

Production Git remote on your development machine

In the dev-side Git repository where you develop your app’s code, create a Git remote pointing to the serverside location where you’ll push production code. (You’ll need to have your Laravel project (on the dev machine) in a Git repo for this workflow to work; take care of this now if you haven’t already.)

# Change into your Laravel project Git repo on your dev machine
you@dev$ cd /path/to/your/laravel-git-repo

# Create a Git remote, called "prod", linked to your app's server
you@dev$ git remote add prod ssh://laravel@laravel_git:/home/laravel/repo/laravel.git

Here’s a breakdown of git remote command:

  • git remote add creates new Git remotes.

  • prod is the remote name. The name is your choice; I chose prod because we’ll use this remote for production code.

  • ssh is the protocol used to connect to the server. This should stay as is.

  • laravel is the name of the serverside user you will create (in a few articles) to administer your web app.

  • laravel_git is the SSH Host created above for pushing code to your production server. It must exactly match the Host field used in the previous section.

  • /home/laravel/repo/laravel.git is the path, on the server, to the serverside Git repo storing your web app.

Need to update the remote’s URL?

You can use git remote set-url anytime you need to update or edit the prod remote’s URL on your development machine:

# Update the URL used for the production remote
you@dev$ git remote set-url prod ssh://laravel@laravel_git:/home/laravel/repo/laravel.git

Push code to server

From your development machine, push your app’s code to the production remote you set up in the previous step (I’m assuming you’re pushing the main branch, adjust if needed).

# Change into your dev-side Git repo
you@dev$ cd path/to/my/laravel-project

# Push your app's `main` branch to the `prod` remote, i.e. to the production server.
# (Assuming you're using a main branch; update branch name (to e.g. master) as necessary.)
you@dev$ git push prod main
Warning: your server and dev Git branch names must match!

The name of the branch in your serverside Git repo must match the name of the branch you’re pushing from your development machine, or the checkout part of the post-receive hook will fail (e.g. both server and dev branches should be main, or both should be master, etc.).

You can check the current branch name in both the serverside and dev-side repos with git branch.

The most likely way you’d run into problems is having a master branch on your server (by default most Git distributions will use master as the default name) and a main branch on your dev machine. You could solve this, for example, by renaming the serverside branch to main, or, more permanently, set main as the default serverside branch name with git config --global init.defaultBranch main and create a new serverside Git repo, which will then have main as the default branch.

Here’s what should happen:

  • Git on your dev machine reads the laravel_git SSH host information you added in the previous step, recognizes which SSH key to use to connect to the server (you might be prompted for the SSH key’s password, or ssh-agent might take of this for you under the hood, depending on your SSH setup).

  • Your app is pushed to the serverside Git repo (SSH into the server and check the contents of git log in /home/laravel/repo/laravel.git).

    (Note that, assuming you’re following along with the tutorial and using a bare serverside Git repo, your app’s files will not appear on the file system in the serverside Git repo because a bare Git repo does not have a working tree. That is expected—you confirm the push’s success with git log, which should show your project’s commit history.)

Ran into problems?
  • Errors with Git’s SSH connection to the server are probably due to an SSH or Git misconfiguration on your dev machine. Double check that the alias in ~/.ssh/config and the Git remote URL match on your dev machine what’s in this article.

  • Serverside errors, at least at this stage, most likely arise from Git branch naming conflicts—see the warning about matching Git branch names above.

Give this and the previous two dev-side steps a reread just be sure, and then please let me know if you’re still having problems pushing code to the server—it might be a mistake in this guide.

Check-in point: check that the output of git log in your serverside Git repo (/home/laravel/repo/laravel.git) shows your project’s commit history.

Checkout code into server directory

We’ll now copy code from the serverside Git repo to the directory from which you’ll serve your app.

SSH into your server, and create a directory to hold your app’s initial release:

# Change into your web server directory
laravel@server$ cd /srv/www/laravel/

# Create a directory to hold your initial release
laravel@server$ mkdir -p releases/initial

Then change into the serverside Git repo and use git checkout to copy your app’s code into the initial release directory.

# Change into your serverside Git repo
laravel@server$ cd ~/repo/laravel.git

# Using `git checkout`, copy your app's code into the initial release directory
laravel@server:laravel.git$ git --work-tree=/srv/www/laravel/releases/initial checkout --force

Your app is almost ready to go live at this point.

Check-in point: at this stage, you should see your app’s files in the initial release directory /srv/www/laravel/releases/initial/.

Your app’s .env file and storage directory (and *.sqlite database file, if using SQLite) are kept in the /srv/www/laravel/shared/ directory, and are shared between releases. You should now create symlinks linking these shared files into place in your initial release:

SHARED=/srv/www/laravel/shared/
RELEASE=/srv/www/laravel/releases/initial/

# Link shared env file into place
laravel@server$ ln -s ${SHARED}/.env ${RELEASE}/.env

# Replace release's boilerplate storage directory with shared storage directory
laravel@server$ rm -rf ${RELEASE}/storage
laravel@server$ ln -s ${SHARED}/storage ${RELEASE}/storage

# If your app uses SQLite, create a parent directory and link database into place
laravel@server$ mkdir -p ${RELEASE}/database/sqlite
laravel@server$ ln -s ${SHARED}/sqlite/database.sqlite ${RELEASE}/database/sqlite/database.sqlite

Run the standard Laravel deployment commands

You’re now ready to run a few standard Laravel deployment commands (most of which you can find in the Laravel docs).

Begin by installing your app’s PHP dependencies using Composer:

# Change into the intial release directory
laravel@server$ cd /srv/www/laravel/releases/initial/

# Install your app's PHP dependencies
laravel@server$ composer install --no-dev --optimize-autoloader

Then install and build your app’s JavaScript dependencies (skip this step if your app has no JavaScript dependencies):

# Install your app's JavaScript dependencies
laravel@server$ npm install

# You may want to also audit your JavaScript dependencies for vulnerabilities
laravel@server$ npm audit fix

# Build your app's JavaScript dependencies
laravel@server$ npm run build

Then cache your config settings, routes, and Blade views:

# Clear any stale cached settings...
laravel@server$ php artisan config:clear
laravel@server$ php artisan route:clear
laravel@server$ php artisan view:clear

# ...then recache config, route, and Blade views
laravel@server$ php artisan config:cache
laravel@server$ php artisan route:cache
laravel@server$ php artisan view:cache

You should then run your database migrations. And if you are seeding your database, now is a good time to do it:

# Run database migrations
laravel@server$ php artisan migrate

# Seed database, if part of your workflow
laravel@server$ php artisan db:seed

Generate your app’s encryption key

Now that you have installed your app’s PHP packages and thus have access to artisan, you should generate your app’s encryption key.

First, open your app’s .env file and ensure it contains a blank APP_KEY= field, which will be populated with your app’s encryption key. Then generate the key:

# Change into the intial release directory
laravel@server$ cd /srv/www/laravel/releases/initial/

# Generate your app's encryption key
laravel@server$ php artisan key:generate

The key should appear in the APP_KEY field of your app’s .env file.

Update ownership and permissions

To quickly review from the permissions and ownership article, your app’s bootstrap/cache, storage and sqlite directory must be writable by the web server process that serves your app.

We’ll address this by giving the Nginx user www-data group ownership of your release files, and group write permissions on these special directories.

# Grant group ownership of your app's files to www-data
laravel@server$ sudo chgrp -R www-data /srv/www/laravel/

# Grant owning group write access special directories
laravel@server$ sudo chmod -R g=rwX /srv/www/laravel/releases/initial/bootstrap/cache
laravel@server$ sudo chmod -R g=rwX /srv/www/laravel/shared/storage
laravel@server$ sudo chmod -R g=rwX /srv/www/laravel/shared/sqlite

Moment of truth

Activate the release:

RELEASE=/srv/www/laravel/releases/initial
ACTIVE=/srv/www/laravel/active

# Point active release to the intial release directory
laravel@server$ ln -sfn ${RELEASE} ${ACTIVE}

For good measure, restart the Nginx web server with sudo nginx -s reload. Your app should then be live when you point a web browser to your app’s domain name or IP address.

Next

The final phase of the tutorial shows how to automate the redeployment process.

Finding this tutorial series useful? Consider saying thank you!

The original writing and media in this series is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.