Executive Summary
This article covers the complete workflow for deploying a Laravel application to a Hetzner Cloud server instance via SSH: provisioning the server with an SSH key, hardening the initial configuration, installing the LEMP stack (Nginx, PHP 8.2+, MySQL/MariaDB, Composer, Node.js), running the Laravel production deployment checklist, configuring a systemd-managed queue worker, and issuing an SSL certificate with Certbot. The Hetzner Cloud console is accessed directly — there is no INNOVATECH portal integration for server provisioning.
This guide walks through the complete process of provisioning a Hetzner Cloud server instance and deploying a Laravel application to it via SSH. It covers server provisioning, initial hardening, LEMP stack installation, Laravel deployment, queue worker configuration, and SSL setup.
The Hetzner Cloud console is accessed directly at console.hetzner.cloud. The INNOVATECH GROUP portal does not manage or provision Hetzner Cloud instances — your portal hosting/cloud service detail page provides reference information only.
Prerequisites
- A Hetzner Cloud account with an active project at
console.hetzner.cloud - An SSH key pair on your local machine (or the ability to generate one)
- A domain name with DNS pointing to the server IP (for SSL)
- Your Laravel application in a Git repository
- Familiarity with Linux command-line administration, SSH, and basic server operations
Step 1: Generate an SSH Key Pair
If you do not already have an SSH key pair on your local machine:
ssh-keygen -t ed25519 -C "deploy@yourdomain.co.za"
Accept the default file location (~/.ssh/id_ed25519) or specify a custom path. Set a passphrase for additional security.
Display the public key to copy it:
cat ~/.ssh/id_ed25519.pub
Step 2: Provision the Hetzner Cloud Server
- Log in to the Hetzner Cloud console at
console.hetzner.cloud. - Select your project and click Add Server.
- Configure the server:
- Location: Choose the data centre closest to your target audience (e.g.
fsn1for Falkenstein, Germany, orhel1for Helsinki). - Image: Ubuntu 22.04 LTS (or the current Ubuntu LTS available).
- Type: Select a server type based on your application's resource requirements.
CX22(2 vCPU, 4 GB RAM) is a reasonable starting point for a Laravel application. - SSH Keys: Click Add SSH Key, paste your public key from Step 1, give it a label, and save. Ensure it is selected for this server.
- Firewall: Optionally assign a Hetzner Cloud firewall group. You can also configure host-level firewall rules after provisioning (covered in Step 4).
- Name: Give the server a descriptive name (e.g.
laravel-prod-01).
- Location: Choose the data centre closest to your target audience (e.g.
- Click Create & Buy Now to provision the server.
Note the server's public IPv4 address once provisioning is complete.
Step 3: Initial SSH Connection
Connect to the server as root using the SSH key:
ssh root@YOUR_SERVER_IP
Verify the connection succeeds. All subsequent commands in this guide are executed on the server via SSH unless otherwise noted.
Step 4: Server Hardening
Create a Deploy User
Running your application as root is a security risk. Create a dedicated deploy user:
adduser deploy
usermod -aG sudo deploy
Copy the SSH authorised key to the new user:
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
Verify you can log in as the deploy user from your local machine before proceeding:
ssh deploy@YOUR_SERVER_IP
Disable Root SSH Login
Edit the SSH daemon configuration:
sudo nano /etc/ssh/sshd_config
Set the following:
PermitRootLogin no
PasswordAuthentication no
Restart SSH:
sudo systemctl restart sshd
Configure UFW Firewall
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
Confirm the rules:
sudo ufw status verbose
Step 5: Install the LEMP Stack
System Updates
sudo apt update && sudo apt upgrade -y
Install Nginx
sudo apt install -y nginx
sudo systemctl enable nginx
Install PHP 8.2+ and Required Extensions
sudo add-apt-repository -y ppa:ondrej/php
sudo apt update
sudo apt install -y php8.2-fpm php8.2-cli php8.2-common php8.2-mysql \
php8.2-xml php8.2-mbstring php8.2-curl php8.2-zip php8.2-gd \
php8.2-bcmath php8.2-intl php8.2-readline php8.2-redis
Verify the installation:
php -v
Install MySQL or MariaDB
sudo apt install -y mariadb-server
sudo systemctl enable mariadb
sudo mysql_secure_installation
Create the application database and user:
sudo mysql -u root -p
CREATE DATABASE laravel_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'laravel'@'localhost' IDENTIFIED BY 'YOUR_SECURE_PASSWORD';
GRANT ALL PRIVILEGES ON laravel_app.* TO 'laravel'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Replace YOUR_SECURE_PASSWORD with a strong, unique password.
Install Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
composer --version
Install Node.js (for Asset Compilation)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node -v && npm -v
Step 6: Deploy the Laravel Application
Clone the Repository
sudo mkdir -p /var/www/laravel-app
sudo chown deploy:deploy /var/www/laravel-app
cd /var/www/laravel-app
git clone git@github.com:your-org/your-app.git .
Install Dependencies
composer install --no-dev --optimize-autoloader
npm ci && npm run build
Configure the Environment
cp .env.example .env
nano .env
Set production values:
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.co.za
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_app
DB_USERNAME=laravel
DB_PASSWORD=YOUR_SECURE_PASSWORD
Generate the application key:
php artisan key:generate
Run the Production Deployment Checklist
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
Set File Permissions
sudo chown -R deploy:www-data /var/www/laravel-app
sudo chmod -R 775 storage bootstrap/cache
Step 7: Configure Nginx
Create an Nginx server block:
sudo nano /etc/nginx/sites-available/laravel-app
server {
listen 80;
server_name yourdomain.co.za www.yourdomain.co.za;
root /var/www/laravel-app/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
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 ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Enable the site and test the configuration:
sudo ln -s /etc/nginx/sites-available/laravel-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Step 8: Configure the Queue Worker
Create a systemd service file for the Laravel queue worker:
sudo nano /etc/systemd/system/laravel-queue.service
[Unit]
Description=Laravel Queue Worker
After=network.target
[Service]
User=deploy
Group=www-data
WorkingDirectory=/var/www/laravel-app
ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3 --max-time=3600
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable laravel-queue
sudo systemctl start laravel-queue
sudo systemctl status laravel-queue
Step 9: Install SSL with Certbot
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.co.za -d www.yourdomain.co.za
Certbot will modify your Nginx configuration to serve HTTPS and set up automatic certificate renewal. Verify the renewal timer:
sudo certbot renew --dry-run
Step 10: Validate the Deployment
- Visit
https://yourdomain.co.zain a browser. You should see your Laravel application. - Check that HTTPS is working correctly (padlock icon, no mixed content warnings).
- Test the queue worker by dispatching a test job and verifying it processes.
- Check the Laravel log for errors:
tail -50 /var/www/laravel-app/storage/logs/laravel.log
Common Failure Modes
| Symptom | Likely Cause | Action |
|---|---|---|
| 502 Bad Gateway | PHP-FPM not running or socket path mismatch | Verify sudo systemctl status php8.2-fpm and check the fastcgi_pass path in Nginx config |
| "Permission denied" on storage/logs | Incorrect file ownership | Run sudo chown -R deploy:www-data storage bootstrap/cache |
| Blank page with no error | APP_DEBUG=false hides errors |
Check storage/logs/laravel.log for the actual error |
| CSS/JS assets not loading | Assets not compiled or Vite manifest missing | Run npm ci && npm run build on the server |
| Queue jobs not processing | systemd service stopped or crashed | Check sudo systemctl status laravel-queue and view logs with journalctl -u laravel-queue |
Security Considerations
- Keep
APP_DEBUG=falsein production. Debug mode exposes environment variables, database credentials, and stack traces. - Use a strong, unique database password. Do not reuse passwords across services.
- Regularly apply system updates:
sudo apt update && sudo apt upgrade. - Consider restricting SSH access by IP in your UFW rules if your team connects from known addresses.
- The
.envfile contains sensitive credentials — ensure it is not accessible via the web server (the Nginxlocation ~ /\.rule above blocks this). - Rotate the
APP_KEYonly if you understand the implications for encrypted data in the database.
When to Contact INNOVATECH GROUP Support
Open a support ticket if:
- You need guidance on selecting a Hetzner Cloud server type for your application's workload
- You encounter networking issues between your Hetzner Cloud server and INNOVATECH-managed services
- You need help configuring DNS records on a domain managed through the INNOVATECH GROUP portal to point to your Hetzner Cloud server
- You require managed server administration beyond self-service deployment