This article walks through deploying a Laravel site built with Docker onto Xserver over SSH and git.
The short answer: nine steps. SSH in, change the PHP version, install Composer, install Node.js, git clone, configure the environment, run migrations, create the storage symlink, and place an .htaccess file.
▼The nine steps
| Step | What you do | Where people get stuck |
|---|---|---|
| 1 | Connect to Xserver over SSH | Key file permissions (600) |
| 2 | Change the PHP version | The control panel and SSH are configured separately |
| 3 | Install Composer | Forgetting to add it to PATH |
| 4 | Install Node.js | — |
| 5 | Deploy with git clone | Registering the public key |
| 6 | Create vendor and .env | .env must not be in git |
| 7 | Run migrations | Wrong database credentials |
| 8 | Create the storage symlink | — |
| 9 | Place an .htaccess file | Pointing the document root at public |
Step 2 is where most people lose time. Xserver manages the PHP used for web requests and the PHP available over SSH independently, so changing one does not change the other.
The host here is Xserver, but the process is close to identical elsewhere. The repository in this example is on Bitbucket; GitHub and GitLab work the same way.
Sponsored
Step 1: Connect to Xserver over SSH
Enable SSH in the Xserver server panel, generate a key pair, and download the private key.
Move the key into ~/.ssh and restrict its permissions. SSH refuses to use a key that other users can read.
mv ~/Downloads/yourserver.key ~/.ssh/
chmod 600 ~/.ssh/yourserver.key
Add an entry to ~/.ssh/config so you do not have to type the details each time:
Host xserver
HostName svXXXX.xserver.jp
User yourserverid
Port 10022
IdentityFile ~/.ssh/yourserver.key
ssh xserver
Step 2: Check and change the PHP version
Laravel has strict PHP requirements, so raise the version on Xserver before anything else.
The original version of this article targeted PHP 7, but current Laravel (13.x) requires PHP 8. The entire PHP 7 line reached end of life in November 2022, so choose the newest PHP 8 release your control panel offers. Read the “7.4.13” in the commands below as whichever version you picked.
Changing it in the server panel
The PHP version is under “PHP Ver. switch” in the Xserver server panel. It is set per domain, so select the domain first.
This only affects web requests. The PHP you get over SSH is separate and is configured next.
Changing it over SSH
Create a bin directory in your home directory:
mkdir $HOME/bin
List the available PHP binaries:
find /opt/php-*/bin -type f -name 'php'
Symlink the one you want, matching the version you set in the control panel:
ln -s /opt/php-7.4.13/bin/php $HOME/bin/php
To remove a symlink you got wrong:
unlink $HOME/bin/php
Put bin at the front of your PATH by editing ~/.bash_profile:
// before
PATH=$PATH:$HOME/bin
// after
PATH=$HOME/bin:$PATH
The order matters. With $HOME/bin at the end, the system PHP is found first and your symlink is ignored.
source ~/.bash_profile
php -v
Sponsored
Step 3: Install Composer
Composer manages PHP packages and is required to install Laravel’s dependencies.
curl -sS https://getcomposer.org/installer | php
“Successfully installed” means it worked. Move it onto your PATH and check:
mv composer.phar $HOME/bin/composer
composer -V
Step 4: Install Node.js
Node.js is needed for npm. This uses nodebrew as the version manager.
Installing nodebrew
wget git.io/nodebrew
perl nodebrew setup
Add it to PATH as the setup output instructs:
echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile
source ~/.bash_profile
nodebrew -v
Installing Node.js
Install an LTS release rather than the latest. As of September 2026 the Active LTS is v24 (Krypton). latest installs the Current release, which is not intended for production.
nodebrew install-binary v24.x.x
nodebrew list
nodebrew use v24.x.x
node -v
Sponsored
Step 5: Deploy with git clone
Generate a key pair on the server and register the public key with your git host:
ssh-keygen -t ed25519 -C "your@email.com"
cat ~/.ssh/id_ed25519.pub
Paste the public key into Bitbucket, GitHub or GitLab, then clone into the domain directory:
cd ~/yourdomain.com/
git clone git@bitbucket.org:youraccount/yourrepo.git
Step 6: Create vendor and .env
vendor and .env are excluded from git by default, so neither arrives with the clone. Both are created on the server.
cd yourrepo
composer install --optimize-autoloader --no-dev
Copy the example env file and generate an application key:
cp .env.example .env
php artisan key:generate
Edit .env with the server’s database credentials and switch the app out of debug mode:
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
DB_DATABASE=yourdb
DB_USERNAME=yourdbuser
DB_PASSWORD=yourdbpassword
APP_DEBUG=false is not optional in production. Left true, an error page exposes file paths, environment variables and database details to anyone who triggers it.
Create the database and user in the Xserver control panel first, and grant the user access to the database.
Step 7: Run migrations
php artisan migrate
A connection error here is almost always the .env credentials. Xserver prefixes database and user names with your server ID, so the names in the control panel are longer than the ones you chose.
Step 8: Create the storage symlink
php artisan storage:link
This links public/storage to storage/app/public so uploaded files are reachable over HTTP.
Step 9: Place an .htaccess file
Laravel serves from its public directory, but Xserver’s document root is public_html. An .htaccess file bridges the two.
Place this in public_html:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/yourrepo/public/
RewriteRule ^(.*)$ /yourrepo/public/$1 [L]
</IfModule>
Never point the document root at the project root itself. Doing so puts .env, with your database password in it, one URL away from anyone who guesses the filename.
When it works locally but not on the server
Three things account for most of it: the PHP version, the .env settings, and which directory is being served.
Check them in that order:
php -vmatches the version set in the control panel.envholds the server’s database credentials, not your local ones- The document root resolves to Laravel’s
publicdirectory
Asset builds are worth a thought too. Vite is the current default, so whether you need npm run build during deployment depends on which config file the project has — vite.config.js or webpack.mix.js.
For the local environment this deploys from, see building a Laravel and Blade environment with Docker. If the build step fails on Node, fixing npm run errors by upgrading Node.js covers it.