Close Menu
GeekBlog

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    How to Create a Content Calendar for Consistent Publishing

    September 4, 2026

    How to Add a Call to Action Button on a Facebook Page

    September 4, 2026

    How to Download a Copy of Your Facebook Data

    September 4, 2026
    Facebook X (Twitter) Instagram Threads
    GeekBlog
    • Home
    • Mobile
    • Tech News
    • Blog
    • Gaming
    • Smartwatch
    • How-To Guides
    • AI & Software
    Facebook
    GeekBlog
    Home»Blog»How to Deploy Laravel on Vultr: 2026 Step by Step
    Blog

    How to Deploy Laravel on Vultr: 2026 Step by Step

    Ethan CaldwellBy Ethan CaldwellSeptember 4, 202610 Mins Read
    Share Facebook Twitter Pinterest LinkedIn Tumblr Email Copy Link
    Share
    Facebook Twitter LinkedIn Pinterest Email Copy Link

    Deploying Laravel on Vultr is a Ubuntu server build: PHP FPM behind Nginx, MySQL for data, Composer for dependencies, supervisor for the queue, one cron line for the scheduler, and Let’s Encrypt for TLS. The piece most tutorials skip is the release directory pattern that makes deploys atomic, and it is the piece that turns a nervous manual upload into a command you can run at 4pm on a Friday. This walkthrough covers all of it in order.

    Quick answer: Deploy an Ubuntu 24.04 Vultr instance, install Nginx, PHP 8.3 FPM, MySQL and Composer, clone the app into /var/www/app/releases/<timestamp>, share storage and .env through symlinks, point /var/www/app/current at the new release, reload PHP FPM, then add supervisor for queue:work and a cron entry for schedule:run.

    Laravel 13 is the current release and requires PHP 8.3 as a minimum, so a modern Ubuntu LTS with the distribution PHP packages lands you in a supported configuration without extra repositories. If you are still on Laravel 12, PHP 8.2 is the floor and everything below still applies. The official deployment documentation is worth reading alongside this for the optimization commands.

    Deploy and secure the instance

    In the Vultr control panel choose Products, Compute, Deploy Server. Cloud Compute is the right product family for almost every Laravel app. Vultr’s Regular Performance line starts at $2.50 per month for 1 vCPU and 0.5 GB with an IPv6 only address, and $5.00 per month for a 1 vCPU and 1 GB tier with IPv4. For a real application, start at 2 GB. PHP FPM workers and MySQL both want memory and 1 GB gets tight fast.

    ssh root@YOUR_VULTR_IP
    
    adduser deploy
    usermod -aG sudo deploy
    rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy/
    
    apt update && apt upgrade -y
    apt install -y ufw fail2ban
    ufw allow OpenSSH
    ufw allow 'Nginx Full'
    ufw --force enable
    
    sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
    sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
    systemctl restart ssh
    Tip: Enable Vultr’s automatic backups or take a snapshot before each significant change. A snapshot restore is measured in minutes. Rebuilding a server from memory at 2am is not.

    Install the stack

    apt install -y nginx mysql-server supervisor unzip git \
      php-fpm php-cli php-mysql php-mbstring php-xml php-curl \
      php-zip php-gd php-bcmath php-intl php-redis
    
    php -v
    mysql_secure_installation
    
    curl -sS https://getcomposer.org/installer | php
    mv composer.phar /usr/local/bin/composer
    composer --version

    Tune PHP for a production application. Edit the FPM ini file and the pool config for your PHP version.

    # /etc/php/8.3/fpm/php.ini
    memory_limit = 256M
    upload_max_filesize = 32M
    post_max_size = 32M
    opcache.enable = 1
    opcache.memory_consumption = 192
    opcache.max_accelerated_files = 20000
    opcache.validate_timestamps = 0
    
    # /etc/php/8.3/fpm/pool.d/www.conf on a 2 GB instance
    pm = dynamic
    pm.max_children = 12
    pm.start_servers = 3
    pm.min_spare_servers = 2
    pm.max_spare_servers = 5
    
    systemctl restart php8.3-fpm
    Warning: Setting opcache.validate_timestamps = 0 makes PHP ignore file changes on disk, which is exactly what you want in production and exactly what will confuse you during a deploy. Your deploy script must reload PHP FPM or the new code will not take effect.

    A few of those choices are worth explaining. opcache.validate_timestamps = 0 stops PHP from checking file modification times on every request, which is a real throughput win and the reason the deploy script has to reload FPM. The pm.max_children figure is the hard ceiling on concurrent PHP requests, so setting it above what your memory supports converts a traffic spike into a swap storm rather than a queue. And installing php-redis now costs nothing even if you do not enable Redis until later.

    Create the database.

    mysql -u root -p
    
    CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'a_long_random_password';
    GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost';
    FLUSH PRIVILEGES;
    EXIT;

    Recommended for you:

    Can You Use Movie Clips in YouTube Videos? What the Law Actually Says
    Blog·Sep 3, 2026

    Can You Use Movie Clips in YouTube Videos? What the Law Actually Says

    Lay out the release directory structure

    This is the structure that makes zero downtime deploys possible. Shared state lives in shared/, each deploy gets its own timestamped directory, and current is a symlink that Nginx follows.

    mkdir -p /var/www/app/{releases,shared/storage}
    chown -R deploy:www-data /var/www/app
    
    # Move the real .env into shared, never into a release
    cp /path/to/your/.env /var/www/app/shared/.env
    chmod 640 /var/www/app/shared/.env
    
    # Laravel's storage skeleton, created once and shared forever
    mkdir -p /var/www/app/shared/storage/{app/public,framework/{cache/data,sessions,views},logs}
    chown -R deploy:www-data /var/www/app/shared
    chmod -R 775 /var/www/app/shared/storage

    The reason storage is shared rather than per release is uploads and logs. If each deploy got a fresh storage directory, every user upload would vanish on the next release. The same logic applies to .env: it holds secrets that should never be in the repository.

    The deploy script

    Save this as /var/www/app/deploy.sh and run it as the deploy user. Each run creates a release, builds it fully, and only then flips the symlink.

    #!/usr/bin/env bash
    set -euo pipefail
    
    APP_DIR=/var/www/app
    REPO=git@github.com:you/your-app.git
    RELEASE="$APP_DIR/releases/$(date +%Y%m%d%H%M%S)"
    
    git clone --depth 1 --branch main "$REPO" "$RELEASE"
    cd "$RELEASE"
    
    ln -nfs "$APP_DIR/shared/.env" "$RELEASE/.env"
    rm -rf "$RELEASE/storage"
    ln -nfs "$APP_DIR/shared/storage" "$RELEASE/storage"
    
    composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist
    
    php artisan migrate --force
    php artisan config:cache
    php artisan route:cache
    php artisan view:cache
    php artisan event:cache
    php artisan storage:link
    
    # Atomic switch
    ln -nfs "$RELEASE" "$APP_DIR/current.tmp"
    mv -Tf "$APP_DIR/current.tmp" "$APP_DIR/current"
    
    sudo systemctl reload php8.3-fpm
    php "$APP_DIR/current/artisan" queue:restart
    
    # Keep the last five releases
    cd "$APP_DIR/releases" && ls -1dt */ | tail -n +6 | xargs -r rm -rf

    Three details carry the weight here. mv -Tf replaces the symlink in a single filesystem operation, so no request ever sees a half updated directory. Reloading PHP FPM clears the opcache, which the earlier validate_timestamps = 0 setting makes mandatory. And queue:restart tells running workers to exit gracefully so supervisor starts them on the new code.

    Nginx and TLS

    Point the server block at current/public, never at a release path directly, or the symlink flip does nothing. Save this as /etc/nginx/sites-available/app. Certbot will rewrite it in place when you issue the certificate, adding the TLS directives and an HTTP to HTTPS redirect, so there is no need to write those by hand.

    server {
        listen 80;
        server_name example.com www.example.com;
        root /var/www/app/current/public;
        index index.php;
    
        add_header X-Frame-Options "SAMEORIGIN";
        add_header X-Content-Type-Options "nosniff";
    
        location / {
            try_files $uri $uri/ /index.php?$query_string;
        }
    
        location ~ \.php$ {
            fastcgi_pass unix:/run/php/php8.3-fpm.sock;
            fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
            fastcgi_param DOCUMENT_ROOT $realpath_root;
            include fastcgi_params;
        }
    
        location ~ /\.(?!well-known).* { deny all; }
    
        client_max_body_size 32M;
    }

    The $realpath_root lines are not decoration. Without them, PHP FPM caches the resolved symlink target and keeps serving the old release after a deploy, which is a maddening bug to diagnose. Then issue the certificate.

    ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/
    rm -f /etc/nginx/sites-enabled/default
    nginx -t && systemctl reload nginx
    
    apt install -y certbot python3-certbot-nginx
    certbot --nginx -d example.com -d www.example.com
    systemctl list-timers | grep certbot

    Queue worker and scheduler

    Laravel needs two background processes and they are configured in different places. Supervisor keeps the queue worker alive and restarts it if it dies. Cron drives the scheduler, which is a separate concern even though both run artisan commands. Skipping either one produces an application that looks healthy in a browser while quietly doing none of its background work.

    # /etc/supervisor/conf.d/app-worker.conf
    [program:app-worker]
    process_name=%(program_name)s_%(process_num)02d
    command=php /var/www/app/current/artisan queue:work --sleep=3 --tries=3 --max-time=3600
    autostart=true
    autorestart=true
    stopasgroup=true
    killasgroup=true
    user=deploy
    numprocs=2
    redirect_stderr=true
    stdout_logfile=/var/www/app/shared/storage/logs/worker.log
    stopwaitsecs=3600
    supervisorctl reread
    supervisorctl update
    supervisorctl status
    
    # Scheduler, as the deploy user
    crontab -u deploy -e
    * * * * * cd /var/www/app/current && php artisan schedule:run >> /dev/null 2>&1
    Note: The scheduler cron runs every minute, always. Laravel decides internally which of your scheduled tasks are due. A common mistake is translating a task’s own frequency into the crontab, which means the scheduler only wakes up occasionally and most tasks never fire.

    Sizing and cost reference

    WorkloadInstancepm.max_childrenDatabase
    Staging or side project1 vCPU, 1 GB5MySQL on the same box
    Small production app1 vCPU, 2 GB12Same box, Redis for cache
    Busy production app2 vCPU, 4 GB25Separate database instance
    Queue heavy workload2 vCPU, 4 GB plus a worker node25Managed database, Redis queue

    Set pm.max_children by dividing the memory you can spare by the average size of a PHP FPM worker, which you can measure with ps once the app is warm. Guessing high is worse than guessing low, because an overcommitted server starts swapping and everything degrades at once.

    If you are running other things on Vultr, the setup here matches our guide to deploying Gatsby on Vultr for the instance and TLS portion. The same Laravel stack underpins installing Bagisto, and if you are comparing PHP frameworks for a VPS, see where to host Phalcon.

    Troubleshooting

    Deploy finishes but the site serves old code. Either PHP FPM was not reloaded, or the Nginx config is missing the $realpath_root parameters. Both cause the same symptom. Fix the config first, then confirm systemctl reload php8.3-fpm is in the deploy script.

    500 error immediately after deploying. Check /var/www/app/shared/storage/logs/laravel.log. The usual causes are a missing .env symlink, a config cache built before the environment file was linked, or a migration that failed. Run php artisan config:clear and rebuild the caches in order.

    Jobs sit in the queue and never process. Supervisor is not running the worker, or the worker is running old code. Check supervisorctl status, then confirm your deploy script calls queue:restart. Workers hold code in memory and will happily run a deleted release forever.

    Recommended for you:

    Why Is Jack the Black Cat Squishmallow So Rare?
    Blog·Sep 3, 2026

    Why Is Jack the Black Cat Squishmallow So Rare?

    “Permission denied” writing to storage or logs. Ownership drift, usually from running an artisan command as root. Reset with chown -R deploy:www-data /var/www/app/shared/storage and chmod -R 775 on the same tree.

    Site slows down badly under modest traffic. Check for swap use with free -h. If the server is swapping, pm.max_children is too high for the memory available. Lower it, restart PHP FPM, and consider moving MySQL to its own instance.

    Frequently asked questions

    What PHP version does Laravel need on Vultr?

    Laravel 13 requires PHP 8.3 as a minimum and supports up to 8.5. Laravel 12 requires 8.2. Ubuntu 24.04 LTS ships PHP 8.3 in its default repositories, which covers both, and the widely used ondrej PPA gives you newer builds if you need a specific point release.

    How big should the Vultr instance be?

    Start at 2 GB of memory for a production application running MySQL on the same box. The 1 GB tier works for staging and small side projects. Memory is almost always the binding constraint before CPU, because PHP FPM workers and the database compete for it.

    Do I need Redis?

    Not to launch. The file cache and database queue driver work fine at low volume. Add Redis when session or cache writes start showing up in your slow query log, or when you want a queue that dispatches faster than database polling allows.

    How do I roll back a bad deploy?

    That is what the releases directory is for. Point the symlink at the previous release and reload PHP FPM. Database migrations are the exception, since they do not roll back with the symlink, which is a good reason to keep migrations backward compatible with the previous release.

    Should I use Laravel Forge instead?

    Forge provisions this exact stack on a Vultr instance and manages the deploy script, certificates and supervisor entries for you. It is a reasonable trade if you value the time. Building it yourself once is still worth doing, because you will need to debug it either way.

    The bottom line

    A Laravel deployment on Vultr is not complicated, it is just detailed. Nginx pointed at current/public with $realpath_root set, PHP FPM with opcache tuned and reloaded on every deploy, shared .env and storage, supervisor for the queue, and one cron line for the scheduler. Each item exists because leaving it out produces a specific and confusing failure.

    Build the release directory pattern on day one rather than retrofitting it. Deploying by pulling into a live directory works right up until a Composer install fails halfway and your site is serving a broken tree. The symlink flip costs you twenty extra lines in a shell script and removes that failure mode entirely, which is the best trade in this whole setup.

    Share. Facebook Twitter Pinterest LinkedIn Tumblr Telegram Email Copy Link
    Previous ArticleHow to Create a Forum in Joomla (2026 Guide)
    Next Article How to Create a Blog in Joomla Without Extensions
    Ethan Caldwell

      Ethan Caldwell is GeekBlog's resident Apple specialist, covering the entire Apple ecosystem - iPhone, iPad, Mac, Apple Watch, AirPods and the software that ties them together. A longtime iOS user and gadget collector, Ethan tracks Cupertino's every move, breaking down Apple keynotes, A- and M-series chip benchmarks, iOS feature updates and the rumor mill into clear, practical takes that help readers decide whether the latest Apple hardware is worth the upgrade.

      Related Posts

      10 Mins Read

      How to Create a Content Calendar for Consistent Publishing

      9 Mins Read

      How to Add a Call to Action Button on a Facebook Page

      10 Mins Read

      How to Download a Copy of Your Facebook Data

      10 Mins Read

      Is Pennsylvania a Good State to Raise a Family?

      9 Mins Read

      AI and National Security: Why Governments Watch Big Tech AI

      10 Mins Read

      How to Implement Facebook Login (SSO) in Your App

      Top Posts

      How to Change HEIC to JPG on iPhone, Mac, Android and Windows (No Software Needed)

      September 3, 20263 Views

      How to Spot AI Generated Images in 2026 (The Old Tricks Stopped Working)

      September 3, 20262 Views

      Check Which Apps Can Read Your Gmail, and Cut Them Off in 60 Seconds

      September 3, 20262 Views
      Stay In Touch
      • Facebook

      Subscribe to Updates

      Get the latest tech news from FooBar about tech, design and biz.

      Most Popular

      How to Change HEIC to JPG on iPhone, Mac, Android and Windows (No Software Needed)

      September 3, 20263 Views

      The EU AI Act Just Became Enforceable, and Most AI Companies Are Not Ready

      August 6, 20263 Views

      How to Spot AI Generated Images in 2026 (The Old Tricks Stopped Working)

      September 3, 20262 Views
      Our Picks

      How to Create a Content Calendar for Consistent Publishing

      September 4, 2026

      How to Add a Call to Action Button on a Facebook Page

      September 4, 2026

      How to Download a Copy of Your Facebook Data

      September 4, 2026

      Subscribe to Updates

      Get the latest creative news from FooBar about art, design and business.

      HEICJPG.online - Convert HEIC to JPG online
      Facebook
      • About Us
      • Contact us
      • Privacy Policy
      • Disclaimer
      • Terms and Conditions
      • Editorial Policy
      • Cookie Policy
      © 2026 GeekBlog

      Type above and press Enter to search. Press Esc to cancel.