How to Use Vultr Object Storage in Laravel

Updated on November 21, 2023
How to Use Vultr Object Storage in Laravel header image

Introduction

Laravel has a powerful file storage abstraction to interact with many types of filesystems. It supports S3-compatible cloud storage like Vultr Object Storage to store static files.

This guide explains how to configure the Laravel file storage system for Vultr Object Storage and use it to store files that users upload from a web portal.

Prerequisites

Before you begin, you should:

Install Nginx and PHP

Add the ondrej repositories to get the latest version of Nginx and PHP.

$ sudo add-apt-repository -y ppa:ondrej/php
$ sudo add-apt-repository -y ppa:ondrej/nginx-mainline
$ sudo apt update

Install Nginx.

$ sudo apt install nginx

Install PHP and its extensions.

$ sudo apt install php-{cli,fpm,mysql,gd,soap,mbstring,bcmath,common,xml,curl,imagick,zip}

Install Composer.

$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
$ php composer-setup.php
$ php -r "unlink('composer-setup.php');"
$ sudo mv composer.phar /usr/local/bin/composer

Configure PHP

  1. Create a new user for the PHP FastCGI Process Manager (PHP-FPM) pool. This user runs the PHP-FPM process and has the website files ownership. It adds extra security and isolation to your website.

     $ sudo adduser website
     $ sudo usermod -a -G website www-data
  2. Create the website directory.

     $ sudo mkdir /var/www/website
  3. Change the directory ownership.

     $ sudo chown website:website /var/www/website
  4. Copy the default PHP-FPM pool configuration as a template for the new pool.

     $ sudo cp /etc/php/8.1/fpm/pool.d/www.conf /etc/php/8.1/fpm/pool.d/website.conf
  5. Open the pool configuration file.

     $ sudo nano /etc/php/8.1/fpm/pool.d/website.conf

    Add the following changes.

    • Change the pool name [www] to [website].
    • Change the line user = www-data to user = website.
    • Change the line group = www-data to group = website.
    • Change the line listen = /run/php/php8.1-fpm.sock to listen = /run/php/php8.1-fpm-website.sock.

    Save the file and exit.

  6. Open the php.ini file.

     $ sudo nano /etc/php/8.1/fpm/php.ini

    Increase PHP maximum upload file size by changing upload_max_filesize = 2M to upload_max_filesize = 10M and post_max_size = 8M to post_max_size = 10M.

  7. Remove the default PHP-FPM pool configuration.

     $ sudo rm /etc/php/8.1/fpm/pool.d/www.conf
  8. Restart PHP-FPM.

     $ sudo systemctl restart php8.1-fpm

Configure Nginx

  1. Disable the default Nginx configuration.

     $ sudo unlink /etc/nginx/sites-enabled/default
  2. Create a new Nginx configuration file.

     $ sudo nano /etc/nginx/sites-available/website
  3. Add the following configurations.

     server {
         listen 80;
         listen [::]:80;
    
         server_name example.com;
    
         root /var/www/website/public;
    
         index index.html index.htm index.php;
    
         charset utf-8;
    
         client_max_body_size 10m;
         client_body_timeout 60s;
    
         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; }
    
         access_log /var/log/example.com-access.log;
         error_log  /var/log/example.com-error.log error;
    
         error_page 404 /index.php;
    
         location ~ \.php$ {
             include snippets/fastcgi-php.conf;
             fastcgi_pass unix:/run/php/php8.1-fpm-website.sock;
             fastcgi_buffers 32 32k;
             fastcgi_buffer_size 32k;
         }
     }

    Make sure you change the domain name example.com to your domain. Save the file and exit.

  4. Enable Nginx configuration.

     $ sudo ln -s /etc/nginx/sites-available/website /etc/nginx/sites-enabled/
  5. Allow Nginx in the firewall.

     $ sudo ufw allow 'Nginx Full'
  6. Restart Nginx.

     $ sudo systemctl restart nginx

Install Node.js

Laravel needs Node.js to compile its front-end assets. Install the latest Node.js version using the Snap package manager.

$ sudo snap install core
$ sudo snap refresh core
$ sudo snap install --classic node

Create Object Storage

  1. Log in to Vultr customer portal.
  2. Navigate to Products -> Objects.
  3. Add Object Storage and give it a label.
  4. Click your Object Storage and navigate to the Bucket tab.
  5. Create a Bucket and give it a name.
  6. Take note of the Hostname, the Secret Key, the Access Key, and the Bucket Name.

Create Web Portal

For the remaining tasks, you need to run them using the website user. Change the user by running the following command.

$ sudo su website

Create New Laravel Project

Go to the website directory.

$ cd /var/www/website

Use Composer to create a new Laravel project.

$ composer create-project laravel/laravel .

Configure Laravel Filesystem

Laravel file storage has an S3 driver to interact with S3-compatible cloud storage like Vultr Object Storage. It requires the Flysystem S3 package. Install it via the Composer package manager.

$ composer require league/flysystem-aws-s3-v3 "^3.0"

Open the .env file.

$ nano .env

Change the FILESYSTEM_DISK value to s3. It tells Laravel file storage to use the S3 driver.

FILESYSTEM_DISK=s3

Add the Vultr Object Storage credentials to the environment variables.

AWS_ENDPOINT=https://sgp1.vultrobjects.com
AWS_ACCESS_KEY_ID=1234567890ABCDEFGH
AWS_SECRET_ACCESS_KEY=ABCDEFGHIJKLMNOPQ
AWS_BUCKET=example

Description of the credentials:

  • AWS_ENDPOINT is your Vultr Object Storage Hostname.
  • AWS_ACCESS_KEY_ID is your Vultr Object Storage Access Key.
  • AWS_SECRET_ACCESS_KEY is your Vultr Object Storage Secret Key.
  • AWS_BUCKET is your Vultr Object Storage Bucket name.

Add Tailwind CSS

This guide uses Tailwind CSS for the CSS framework. Install it via NPM.

$ npm install -D tailwindcss postcss autoprefixer

Create and open the Tailwind CSS configuration file.

$ npx tailwindcss init -p
$ nano tailwind.config.js

Change the content to:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./resources/**/*.blade.php",
    "./resources/**/*.js",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Open the resources/css/app.css file and add the Tailwind CSS directives.

@tailwind base;
@tailwind components;
@tailwind utilities;

Add JavaScript

Open the resources/js/app.js file and add the following code:

document.getElementById('fileImage').addEventListener('change',function(){
    if( this.files.length > 0 ){
        document.getElementById('uploadBtn').removeAttribute('disabled');
    }
});

Create Views

Create resources/views/gallery.blade.php file with the following content:

<html>
    <head>
        <title>Gallery</title>

        @vite(['resources/css/app.css', 'resources/js/app.js'])
    </head>
    <body>
        <div class="max-w-7xl m-auto">
            <h1 class="text-3xl font-bold text-gray-900 text-center py-8 uppercase">Gallery</h1>
            <form action="" method="post" enctype="multipart/form-data" class="flex flex-wrap text-center items-center justify-center p-4 rounded-lg items-center">
                @csrf
                <label class="block">
                  <input id="fileImage" type="file" name="fileImage" class="block w-full text-sm text-slate-500 pr-6
                    file:cursor-pointer
                    file:mr-4 file:py-2 file:px-4
                    file:rounded-full file:border-0
                    file:text-sm file:font-semibold
                    file:bg-indigo-50 file:text-indigo-700
                    hover:file:bg-indigo-100
                  "/>
                  @if ($errors->has('fileImage'))
                    <span class="block text-red-700 py-4 text-left">{{ $errors->first('fileImage') }}</span>
                  @endif
                </label>
                <button id="uploadBtn" disabled class="rounded border border-transparent bg-indigo-600 px-6 py-2 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50" type="submit">
                    Upload Image
                </button>
            </form>

            <div class="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8">
                @foreach ($images as $image)
                    <div>
                        <img class="rounded" src="<?php echo Storage::url($image); ?>">
                    </div>
                @endforeach
            </div>
        </div>
    </body>
</html>

Compile the front-end assets.

$ npm run build

Create Controller

Create a GalleryController.

$ php artisan make:controller GalleryController

Open the GalleryController file.

$ nano app/Http/Controllers/GalleryController.php

Load the Storage facade in the GalleryController file. It contains a set of functions you need to interact with Object Storage.

use Illuminate\Support\Facades\Storage;

Create the index action. It shows the upload form and lists all images from the gallery folder in your Bucket. It uses the files method from the Storage facade to get an array of all files in a directory.

public function index()
{
    $images = Storage::files('gallery');
    return view('gallery', compact('images'));
}

Create the upload action. It handles when a user uploads their images. It validates the file and then stores it in Vultr Object Storage using the putFile method from the Storage facade.

public function upload(Request $request)
{
    $validated = $request->validate([
        'fileImage' => 'required|image',
    ]);

    Storage::putFile('gallery', $validated['fileImage'], 'public');

    return redirect('/');
}

The following is the full content of the GalleryController file:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class GalleryController extends Controller
{
    public function index()
    {
        $images = Storage::files('gallery');

        return view('gallery', compact('images'));
    }

    public function upload(Request $request)
    {
        $validated = $request->validate([
            'fileImage' => 'required|image',
        ]);

        Storage::putFile('gallery', $validated['fileImage'], 'public');

        return redirect('/');
    }
}

Defining Routes

Open routes/web.php.

$ nano routes/web.php

Update it with the following code:

<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\GalleryController;

Route::get('/', [GalleryController::class, 'index']);
Route::post('/', [GalleryController::class, 'upload']);

Test Web Portal

  1. Open your domain in a browser.
  2. You should see the upload form.
  3. Upload an image.
  4. Check if the image appears in your Vultr Object Storage and the gallery below the upload form.

Secure Web Portal with Let's Encrypt

Let's Encrypt provides a free SSL certificate for your website. To generate the certificate, you need to use the Certbot software tool.

  1. Install Certbot.

     $ sudo snap install core; sudo snap refresh core
     $ sudo snap install --classic certbot
     $ sudo ln -s /snap/bin/certbot /usr/bin/certbot
  2. Generate the SSL certificate.

     $ sudo certbot --nginx
  3. Visit your domain in the browser and confirm it has an HTTPS connection.

Let's Encrypt certificate expires after 90 days. Certbot adds the renewal command to the systemd timer or Cron Job to renew the certificate automatically before it expires. You can verify it with the following commands:

$ systemctl list-timers | grep 'certbot\|ACTIVATES'
$ ls -l /etc/cron.d/certbot

Conclusion

Laravel file storage provides a single interface to interact with different storage systems. It has an S3 driver that you can use to integrate Vultr Object Storage in Laravel.

Further Reading