How to Use Vultr Object Storage in AdonisJS
Introduction
Drive is a storage abstraction library in AdonisJS. It provides a consistent API that works across all storage providers.
Drive has an S3 driver to support S3-compatible cloud storage like Vultr Object Storage. This guide explains how to configure the AdonisJS Drive for Vultr Object Storage and use it to store and read files.
Prerequisites
Before you begin, you should:
Install Node.js
AdonisJS requires at least Node.js version 14. You can install the latest version of Node.js using Node Version Manager (NVM).
Install NVM:
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
Disconnect and reconnect your ssh session.
Install Node.js:
$ nvm install node
Check the Node.js version:
$ node -v v19.3.0
Create Object Storage
- Log in to Vultr customer portal.
- Navigate to Products -> Objects.
- Add Object Storage. Choose the region and give it a label.
- Click your Object Storage and navigate to the Bucket tab.
- Create a Bucket and give it a name.
- Take note of the
Hostname
, theSecret Key
, theAccess Key
, and theBucket Name
.
Create New AdonisJS App
Go to the home directory.
$ cd ~
Create a new directory for your app.
$ mkdir app
Go to the app
directory and generate a new AdonisJS app using the npm init
command.
$ cd app
$ npm init adonis-ts-app@latest website
- Select the web project structure.
- Choose `y' when it prompts you to configure the Webpack Encore.
It generates your app in the website
directory. For the remaining tasks, you need to run them in the website
directory.
$ cd website
Install Redis®
The sample app in this guide uses Redis® to store the image filenames.
Install Redis®:
$ sudo apt install redis-server
Set up Redis® persistent mode:
Open Redis® configuration file:
$ sudo nano /etc/redis/redis.conf
Change the
appendonly no
toappendonly yes
.Save file and exit.
Restart Redis®.
$ sudo systemctl restart redis-server
Install and configure the AdonisJS Redis® package:
$ npm i @adonisjs/redis
$ node ace configure @adonisjs/redis
Open the env.ts
file:
$ nano env.ts
Add the following rules:
REDIS_CONNECTION: Env.schema.enum(['local'] as const),
REDIS_HOST: Env.schema.string({ format: 'host' }),
REDIS_PORT: Env.schema.number(),
REDIS_PASSWORD: Env.schema.string.optional(),
Configure AdonisJS Drive
AdonisJS Drive has an S3 driver to interact with S3-compatible cloud storage like Vultr Object Storage.
Install and configure the S3 driver:
$ npm i @adonisjs/drive-s3
$ node ace configure @adonisjs/drive-s3
Open the env.ts
file:
$ nano env.ts
Update the DRIVE_DISK
rules:
DRIVE_DISK: Env.schema.enum(['local','s3'] as const),
Add the following rules:
S3_KEY: Env.schema.string(),
S3_SECRET: Env.schema.string(),
S3_BUCKET: Env.schema.string(),
S3_REGION: Env.schema.string(),
S3_ENDPOINT: Env.schema.string.optional(),
Open the config/drive.ts
file:
$ nano config/drive.ts
Add the S3 configurations inside the disks
object:
s3: {
driver: 's3',
visibility: 'public',
key: Env.get('S3_KEY'),
secret: Env.get('S3_SECRET'),
region: Env.get('S3_REGION'),
bucket: Env.get('S3_BUCKET'),
endpoint: Env.get('S3_ENDPOINT'),
}
Open the .env
file:
$ nano .env
Update the DRIVE_DISK
value to s3
:
DRIVE_DISK=s3
Add the Vultr Object Storage credentials:
S3_KEY=
S3_SECRET=
S3_BUCKET=adonis-drive
S3_REGION=sgp1
S3_ENDPOINT=https://sgp1.vultrobjects.com
S3_KEY
is your Vultr Object Storage Access Key.S3_SECRET
is your Vultr Object Storage Secret Key.- S3_BUCKET` is your Vultr Object Storage Bucket name.
S3_ENDPOINT
is your Vultr Object Storage Hostname.S3_REGION
is your Vultr Object Storage Region.
Add Tailwind CSS
This guide uses Tailwind CSS for the CSS framework. Install Tailwind CSS and its dependencies via NPM:
$ npm install -D tailwindcss postcss autoprefixer postcss-loader
Open the webpack.config.js
file:
$ nano webpack.config.js
Enable the PostCSS loader:
Encore.enablePostCssLoader()
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/**/*.edge",
"./resources/**/*.js",
],
theme: {
extend: {},
},
plugins: [],
}
Open the resources/css/app.css
file and replace the content to Tailwind CSS directives:
@tailwind base;
@tailwind components;
@tailwind utilities;
Add JavaScript
Open the resources/js/app.js
file and add the following scripts:
import '../css/app.css'
document.getElementById('fileImage').addEventListener('change',function(){
if( this.files.length > 0 ){
document.getElementById('uploadBtn').removeAttribute('disabled');
}
});
The scripts enable the upload button after the user selects the image file.
Create View
Create a resources/views/gallery.edge
file:
$ nano resources/views/gallery.edge
Add the following code:
<html>
<head>
<title>Gallery</title>
@entryPointStyles('app')
</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 justify-center items-start p-4 rounded-lg">
<label class="block py-1">
<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 (flashMessages.has('errors.fileImage'))
<span class="block text-red-700 py-4 text-left">{{ flashMessages.get('errors.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 ">
@each(image in images)
<div>
<img class="rounded" src="{{ image }}">
</div>
@end
</div>
</div>
@entryPointScripts('app')
</body>
</html>
Create Controller
Create and open the GalleryController.ts
file:
$ node ace make:controller GalleryController -e
$ nano app/Controllers/Http/GalleryController.ts
Import libraries and helpers at the top of the file:
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Drive from '@ioc:Adonis/Core/Drive'
import Redis from '@ioc:Adonis/Addons/Redis'
import { string } from '@ioc:Adonis/Core/Helpers'
import { schema } from '@ioc:Adonis/Core/Validator'
Create the index
action. It shows the upload form and lists all images from your Vultr Object Storage. It gets the image filenames from Redis® and calls the getUrl
method from the Drive library to get the URL of each image.
public async index({ view }: HttpContextContract) {
const galleryString = await Redis.get('gallery')
const gallery = (galleryString) ? JSON.parse(galleryString) : []
let images:string[] = []
for (const filename of gallery) {
const url = await Drive.getUrl(`gallery/${filename}`)
images.push(url)
}
return view.render('gallery', { images })
}
Create the upload
action. It handles when a user uploads their images. It validates the file, saves the filename to Redis®, and then stores the file in Vultr Object Storage using the moveToDisk
method.
public async upload({ request, response }: HttpContextContract) {
const imageSchema = schema.create({
fileImage: schema.file({
extnames: ['jpg', 'png', 'gif']
}),
})
const payload = await request.validate({ schema: imageSchema })
const filename = `${string.generateRandom(32)}.${payload.fileImage.extname}`
if (payload.fileImage) {
await payload.fileImage.moveToDisk(", {
name: `gallery/${filename}`
}, 's3')
const galleryString = await Redis.get('gallery')
let gallery:string[] = []
if (galleryString) {
gallery = JSON.parse(galleryString)
}
gallery.push(filename)
await Redis.set('gallery', JSON.stringify(gallery))
}
return response.redirect().toPath('/')
}
You need to save and get the image filenames from Redis® because the AdonisJS S3 driver cannot get a list of files in a bucket or folder. It can only get one file at a time.
The following is the full content of the GalleryController.ts
file:
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Drive from '@ioc:Adonis/Core/Drive'
import Redis from '@ioc:Adonis/Addons/Redis'
import { string } from '@ioc:Adonis/Core/Helpers'
import { schema } from '@ioc:Adonis/Core/Validator'
export default class GalleryController {
public async index({ view }: HttpContextContract) {
const galleryString = await Redis.get('gallery')
const gallery = (galleryString) ? JSON.parse(galleryString) : []
let images:string[] = []
for (const filename of gallery) {
const url = await Drive.getUrl(`gallery/${filename}`)
images.push(url)
}
return view.render('gallery', { images })
}
public async upload({ request, response }: HttpContextContract) {
const imageSchema = schema.create({
fileImage: schema.file({
extnames: ['jpg', 'png', 'gif']
}),
})
const payload = await request.validate({ schema: imageSchema })
const filename = `${string.generateRandom(32)}.${payload.fileImage.extname}`
if (payload.fileImage) {
await payload.fileImage.moveToDisk(", {
name: `gallery/${filename}`
}, 's3')
const galleryString = await Redis.get('gallery')
let gallery:string[] = []
if (galleryString) {
gallery = JSON.parse(galleryString)
}
gallery.push(filename)
await Redis.set('gallery', JSON.stringify(gallery))
}
return response.redirect().toPath('/')
}
}
Add Routes
Open the start/routes.ts
file:
$ nano start/routes.ts
Add the following code:
Route.get('/', 'GalleryController.index')
Route.post('/', 'GalleryController.upload')
Test the Application
To test your app, you need to disable the Ubuntu firewall.
$ sudo ufw disable
You can enable it again when you build your app for production.
Start a development server:
$ node ace serve --encore-args="--host [VULTR_VPS_IP_ADDRESS]"
Open the
http://[VULTR_VPS_IP_ADDRESS]:3333
in a browser.You should see the upload form.
Upload an image.
Check if the image appears in your Vultr Object Storage and the gallery below the upload form.
Press the Ctrl+C to stop the development server.
Use Multiple Object Storage Locations
Vultr Object Storage is available in multiple locations. The followings are the locations that Vultr supports:
- Amsterdam:
ams1.vultrobjects.com
- New Jersey:
ewr1.vultrobjects.com
- Silicon Valley:
sjc1.vultrobjects.com
- Singapore:
sgp1.vultrobjects.com
You can use multiple object storage locations in the AdonisJS app to add redundancy to your files. To do that, you add a disk configuration for each location in the config/drive.ts
file. Disk in AdonisJS Drive represents a particular storage driver and location.
Create New Object Storage
- Go to Vultr customer portal.
- Navigate to Products -> Objects.
- Add Object Storage. Choose a different region, for example, Amsterdam.
- Create a bucket in the new object storage.
- Take note of the
Hostname
, theSecret Key
, theAccess Key
, and theBucket Name
.
Configure New Disk
Open the config/drive.ts
file:
$ nano config/drive.ts
Add a new disk configuration in the S3 section. You can set the disk name to anything, for example, s3ams
. Add a suffix to the environment variable names to differentiate them from the first disk.
s3ams: {
driver: 's3',
visibility: 'public',
key: Env.get('S3_KEY_AMS'),
secret: Env.get('S3_SECRET_AMS'),
region: Env.get('S3_REGION_AMS'),
bucket: Env.get('S3_BUCKET_AMS'),
endpoint: Env.get('S3_ENDPOINT_AMS'),
},
Open the .env
file:
$ nano .env
Add the credentials of your object storage:
S3_KEY_AMS=
S3_SECRET_AMS=
S3_BUCKET_AMS=adonis-drive
S3_REGION_AMS=ams1
S3_ENDPOINT_AMS=https://ams1.vultrobjects.com
S3_KEY_AMS
is your Vultr Object Storage Access Key.S3_SECRET_AMS
is your Vultr Object Storage Secret Key.S3_BUCKET_AMS
is your Vultr Object Storage Bucket name.S3_ENDPOINT_AMS
is your Vultr Object Storage Hostname.S3_REGION_AMS
is your Vultr Object Storage Region.
Open the env.ts
file:
$ nano env.ts
Add the s3ams
disk name to the DRIVE_DISK
enum value:
DRIVE_DISK: Env.schema.enum(['local','s3','s3ams'] as const),
Add validation rules for s3ams
disk environment variables:
S3_KEY_AMS: Env.schema.string(),
S3_SECRET_AMS: Env.schema.string(),
S3_BUCKET_AMS: Env.schema.string(),
S3_REGION_AMS: Env.schema.string(),
S3_ENDPOINT_AMS: Env.schema.string.optional(),
Save the file and exit.
Update Controller
Open the GalleryController.ts
file:
$ nano app/Controllers/Http/GalleryController.ts
In the upload
action, search for the code that is responsible for uploading the file to the object storage.
await payload.fileImage.moveToDisk(", {
name: `gallery/${filename}`
}, 's3')
Duplicate the code and change the disk name to s3ams
.
await payload.fileImage.moveToDisk(", {
name: `gallery/${filename}`
}, 's3')
await payload.fileImage.moveToDisk(", {
name: `gallery/${filename}`
}, 's3ams')
It uploads the file to both of your object storage locations.
Set the Default Disk
The index
action in the GalleryController.ts
uses the default disk to get the image URL. To change the default disk, open the .env
file:
$ nano .env
Update the DRIVE_DISK
value to the disk that you want:
DRIVE_DISK=s3ams
Save the file and exit.
Test the App
Start a development server:
$ node ace serve --encore-args="--host [VULTR_VPS_IP_ADDRESS]"
Open the
http://[VULTR_VPS_IP_ADDRESS]:3333
in a browser.Upload an image.
Check if the image appears in both Vultr Object Storage locations and the gallery below the upload form.
Press the Ctrl+C to stop the development server.
Build for Production
AdonisJS application uses TypeScript. You need to compile it to JavaScript before running it in production.
Go to the website
folder:
$ cd ~/app/website
Compile it using the build
command. The result is in the build
folder.
$ node ace build --production --ignore-ts-errors
Copy the .env
file to the build
folder and open it:
$ cp .env build/.env
$ nano build/.env
Set the NODE_ENV
variable in the .env
file to production
:
NODE_ENV=production
Install production-only dependencies to the build
folder:
$ cd build
$ npm ci --production
Run App on Production Using PM2
PM2 is a daemon process manager. It helps you run and manage your AdonisJS application on production.
Install the latest PM2 package:
$ npm install pm2@latest -g
Create the PM2 ecosystem file to manage your app:
$ cd ~/app
$ nano ecosystem.config.js
Add these configurations to the ecosystem.config.js
file:
module.exports = {
apps : [
{
name : "website",
script : "./website/build/server.js"
}
]
}
- Put your app name in the
name
parameter. - Put the production path of
script.js
in thescript
parameter.
Run your app:
$ pm2 start ecosystem.config.js
$ pm2 list
At this point, your app is running. But, the process is not persistent yet. It means that you must run your app manually again after you restart your server.
To have a persistent app running, you need to generate a startup script for PM2.
$ pm2 startup
Copy and paste the displayed command onto the terminal:
$ sudo env PATH=$PATH:/home/ubuntu/.nvm/versions/node/v19.3.0/bin /home/ubuntu/.nvm/versions/node/v19.3.0/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu
Save your PM2 app list using this command:
$ pm2 save
Nginx Reserve Proxy
You need to set up an Nginx reserve proxy to connect your domain to your app. You put your apps behind the Nginx web server. It accepts all incoming requests and forwards them to your apps.
Add the ondrej repositories to get the latest version of Nginx.
$ sudo add-apt-repository -y ppa:ondrej/nginx-mainline
$ sudo apt update
Install Nginx:
$ sudo apt install nginx
Disable the default Nginx configuration:
$ sudo unlink /etc/nginx/sites-enabled/default
Create a new Nginx configuration file:
$ sudo nano /etc/nginx/sites-available/website
Add the following configurations. Make sure you change the domain name example.com
to your domain. Save the file and exit.
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:3333;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;
}
}
Enable Nginx configuration:
$ sudo ln -s /etc/nginx/sites-available/website /etc/nginx/sites-enabled/
Test your configurations from syntax errors:
$ sudo nginx -t
If there are no errors then you can reload the Nginx process:
$ sudo systemctl reload nginx
Point your domains to your Vultr VPS IP address.
Configure Firewall
Set the firewall to allow ssh port:
$ sudo ufw allow 'OpenSSH'
Allow HTTP and HTTPS ports:
$ sudo ufw allow 'Nginx Full'
Enable the firewall:
$ sudo ufw enable
Check firewall status:
$ sudo ufw status
Secure Apps with Let's Encrypt SSL Certificate
Let's Encrypt provides a free SSL certificate for your website. To generate the certificate, you need to use the Certbot software tool.
Install Certbot:
$ sudo snap install core; sudo snap refresh core
$ sudo snap install --classic certbot
$ sudo ln -s /snap/bin/certbot /usr/bin/certbot
Generate the SSL certificate:
$ sudo certbot --nginx
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 command:
$ systemctl list-timers | grep 'certbot\|ACTIVATES'
Conclusion
This guide shows examples of using the Vultr Object Storage in AdonisJS with single or multiple object storage locations, including the steps to make the app production-ready.
Further Reading
Vultr Object Storage. AdonisJS Drive Documentation. Deploy Multiple Adonis.js Applications with PM2 and Nginx. PM2 Documentation. NGINX Reverse Proxy.