How to Deploy Vendure on Ubuntu 22.04

Updated on October 6, 2023
How to Deploy Vendure on Ubuntu 22.04 header image

Introduction

Vendure is an open-source headless commerce framework that allows to build production-ready e-commerce applications powered by Node.js, TypeScript and GraphQL. It works by exposing all application functionality through APIs. Vendure exposes all the shop front-end functionalities on the GraphQL API and doesn't offer a default storefront interface. This improves developer flexibility because it allows you to create your storefront using any front-end technology.

This guide explains how to deploy Vendure on a Ubuntu 22.04 Vultr Server. You are to integrate the framework with Vultr Object Storage, a Managed Database for PostgreSQL, and Redis to build a production-ready application.

Prerequisites

Before you begin:

Install Node.js

Vendure required the Node.js version 16.x or above. Install the latest Node.js version as described in the steps below.

  1. Update the server

     $ sudo apt update
  2. Create the keyrings directory

     $ sudo mkdir -p /etc/apt/keyrings
  3. Download and add the Node source GPG key to your server keys

     curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
  4. Add the Node.js repository to your apt sources list. Replace 20 with your desired version

     NODE_MAJOR=20
     echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
  5. Update the server packages

     $ sudo apt update
  6. Install Node.js

     $ sudo apt install nodejs -y
  7. Verify the installed Node.js version

     $ nodejs --version

    Output:

     v20.5.1

Set Up the Local PostgreSQL Database Server

To install Vendure on the server, you need to temporarily save the application data on your local PostgreSQL database server. Later, you can migrate the data to a Vultr Managed Database for MySQL before deploying the application to production. Set up the local database as described below.

Enable Password Authentication on PostgreSQL

  1. Verify the installed PostgreSQL version

     $ psql --version

    Output:

     psql (PostgreSQL) 14.16 (Ubuntu 14.16-0ubuntu0.22.04.1)
  2. Depending on your PostgreSQL version, edit the pg_hba.conf file using a text editor such as nano

     $ sudo nano /etc/postgresql/14/main/pg_hba.conf
  3. Find the following configurations line

     # "local" is for Unix domain socket connections only
     local   all             all                                     peer
  4. Change the peer authentication method to md5

     local   all             all                                     md5

    Save and close the file

  5. Restart the PostgreSQL database server

     $ sudo systemctl restart postgresql

Create a New PostgreSQL Database

  1. Log in to the PostgreSQL database server

     $ sudo -u postgres psql
  2. Create a new database

     postgres=# CREATE DATABASE vendure_db;

    Output:

     CREATE DATABASE
  3. Create a new database user with a strong password

     postgres=# CREATE USER vendure_user WITH ENCRYPTED PASSWORD 'strong-password';

    Output:

     CREATE ROLE
  4. Grant the user full privileges to the Vendure database

     postgres=# GRANT ALL PRIVILEGES ON DATABASE vendure_db TO vendure_user;

    Output:

     GRANT
  5. Quit the PostgreSQL console

     postgres-# \q

Install Vendure

  1. Using the npx Node.js tool, Install Vendure using the @vendure/create tool.

     $ npx @vendure/create vendure-app
  2. Reply to each of the installation prompts as described below

    Press Y to install the @vendure/create package

     Need to install the following packages:
       @vendure/create@2.0.2
     Ok to proceed? (y) y

    Select Postgres as a database in use

     ◆  Which database are you using?
     │  ○ MySQL
     │  ○ MariaDB
     │  ● Postgres
     │  ○ SQLite
     │  ○ SQL.js

    Press Enter to set localhost as the PostgreSQL hostname

     ◆  What's the database host address?
     │  localhost

    Press Enter to keep 5432 as the PostgreSQL port number

     ◆  What port is the database listening on?
     │  5432

    Enter the PostgreSQL database name you created earlier

     ◆  What's the name of the database?
     │  vendure_db

    Press Enter to keep the PostgreSQL schema set to public

        What's the schema name we should use?
     │  public

    Enter the PostgreSQL user you created earlier

     ◇  What's the database user name?
     │  vendure_user

    Enter the PostgreSQL database user password you created earlier

     ◇  What's the database password?
     │  strong-password

    Press Enter to keep superadmin as the default administrator username. Change the username to your desired value

     ◇  What identifier do you want to use for the superadmin user?
     │  superadmin
     │

    Enter your desired super administrator username or press Enter to use the default password

     ◇  What password do you want to use for the superadmin user?
     │  superadmin
     │

    Select yes and press Enter to populate the database with sample product data

     ◆  Populate with some sample product data?
     │  ● yes
     │  ○ no
  3. When successful, the installation process should complete with the following output:

     ◇  Server successfully initialized and populated
     │
     ◇   ──────────────────────────────────────────╮
     │                                             │
     │  Success! Created a new Vendure server at:  │
     │                                             │
     │                                             │
     │  /home/example_user/vendure-app             │
     │                                             │
     │                                             │
     │  We suggest that you start by typing:       │
     │                                             │
     │                                             │
     │  $ cd vendure-app                           │
     │  $ npm run dev                              │
     │                                             │
     ├─────────────────────────────────────────────╯
     │
     └  Happy hacking!
  4. List files in your working directory

     $ ls

    Output:

     vendure-app

    Verify that a new vendure-app directory is available on the list

Set Up the Vultr Managed Database for PostgreSQL

  1. Log in to your Vultr Managed Database for PostgreSQL. Replace vultradmin, 1234, host.vultrdb.com with your actual values

     $ psql -h host.vultrdb.com -d postgres -U vultradmin

    Or, copy and use your database connection string from your Vultr Managed Database for PostgreSQL control panel

    Get PostgreSQL URL

  2. When logged in, create a new database

     defaultdb=> CREATE DATABASE venduredb;

    Output:

     CREATE DATABASE
  3. Quit PostgreSQL console

     defaultdb=> \q

Migrate Data the local PostgreSQL Database Data to your Vultr PostgreSQL Managed Database

  1. Back up your PostgreSQL database to the venduredb.sql file using the user and database you created earlier

     $ pg_dump --no-owner -U vendure_user -d vendure_db -W > venduredb.sql

    When prompted, enter the correct Vendure user database password you set earlier

  2. Using the backup file, restore the database to your Vultr PostgreSQL Managed Database. Replace host.vultrdb.com,vendure_db,vultradmin with your actual details.

     $ psql -h host.vultrdb.com -d vendure_db -U vultradmin < venduredb.sql
  3. When the restoration is complete, access your Vultr Managed Database for PostgreSQL

     $ psql -h host.vultrdb.com -d vendure_db -U vultradmin 
  4. View the Vendure database tables

     vendure_db=> \dt

    Output:

                               List of relations
      Schema |                    Name                     | Type  | Owner 
     --------+---------------------------------------------+-------+-------
      public | address                                     | table | vultradmin
      public | administrator                               | table | vultradmin
      public | asset                                       | table | vultradmin
      public | asset_channels_channel                      | table | vultradmin
      public | asset_tags_tag                              | table | vultradmin
      public | authentication_method                       | table | vultradmin
      public | channel                                     | table | vultradmin
      public | collection                                  | table | vultradmin
      public | collection_asset                            | table | vultradmin
      public | collection_channels_channel                 | table | vultradmin
      public | collection_closure                          | table | vultradmin
      public | collection_product_variants_product_variant | table | vultradmin
      public | collection_translation                      | table | vultradmin
      public | customer                                    | table | vultradmin
      public | customer_channels_channel                   | table | vultradmin
      public | customer_group                              | table | vultradmin
      public | customer_groups_customer_group              | table | vultradmin
      public | facet                                       | table | vultradmin
      public | facet_channels_channel                      | table | vultradmin
     :

    Enter Q to exit the PostgreSQL pager

  5. Exit the PostgreSQL console

     # \q
  6. In your Vultr customer portal, download the Vultr Managed Database for PostgreSQL signed certificate file ca-certificate.crt

    Download certificate

  7. When downloaded, in your terminal session, use scp and upload the file to your server

     $ scp ca-certificate.crt example_user@SERVER-IP:/home/example_user/
  8. When uploaded, verify that the certificate file is available in your user home directory

     $ ls

    Output:

     ca-certificate.crt
  9. Move the certificate file to the /usr/local/share/ca-certificates/ directory.

     $ sudo mv ca-certificate.crt /usr/local/share/ca-certificates/
  10. Edit the vendure-config.ts file

     $ nano src/vendure-config.ts
  11. Import the readFileSync method from the node:fs at the top of the file

     import { readFileSync } from 'node:fs';
  12. Add the following declarations to the dbConnectionOptions section

     ssl: {
         rejectUnauthorized: true,
         ca: readFileSync('/usr/local/share/ca-certificates/ca-certificate.crt').toString(),
     },

    Your edited dbConnectionOptions section should look like the one below:

     dbConnectionOptions: {
         type: 'postgres',
         // See the README.md "Migrations" section for an explanation of
         // the `synchronize` and `migrations` options.
         synchronize: false,
         migrations: [path.join(__dirname, './migrations/*.+(js|ts)')],
         logging: false,
         database: process.env.DB_NAME,
         schema: process.env.DB_SCHEMA,
         host: process.env.DB_HOST,
         port: +process.env.DB_PORT,
         username: process.env.DB_USERNAME,
         password: process.env.DB_PASSWORD,
         ssl: {
             rejectUnauthorized: true,
             ca: readFileSync('/usr/local/share/ca-certificates/ca-certificate.crt').toString(),
         },
     },

    Save and close the file.

Switch the Vendure PostgreSQL Database Configuration

  1. Switch to the vendure-app directory

     $ cd vendure-app
  2. Edit the .env file

     $ nano .env
  3. Update the following existing variables with your Vultr Managed Database for PostgreSQL details

     DB_HOST=host.vultrdb.com   
     DB_PORT=1234
     DB_NAME=vendure_db
     DB_USERNAME=vultradmin
     DB_PASSWORD=managed-db-password

    Save and close the file.

Store Vendure Assets in Object Storage

Create a New Bucket

  1. Log in to the Vultr customer portal

  2. Navigate to Products -> Cloud Storage -> Object Storage.

    Object storage menu

  3. Access your Vultr Object Storage control panel

    Select object storage

  4. Navigate to Buckets

    Bucket tab

  5. Click the Create Bucket button, and assign the bucket a name of your choice. For example vendure

    Create bucket

Sync the Assets to Vultr Object Storage

  1. View your working directory

     $ pwd

    Verify that you're operating in the vendure-app directory, or switch to the directory

     $ cd vendure-app
  2. Using the s3cmd tool, synchronize the Vendure assets directory to your Vultr Object Storage bucket

     $ s3cmd sync static/assets/ s3://vendure/

    Verify that the file transfer completes successfully

Add the Vultr Object Storage Configuration

  1. Using npm, install the @aws-sdk/client-s3 and @aws-sdk/lib-storage packages

     $ npm install @aws-sdk/client-s3 @aws-sdk/lib-storage --save
  2. Edit the environment.d.ts file

     $ nano src/environment.d.ts
  3. Add the following configurations within the ProcessEnv function

     S3_ENDPOINT: string;
     S3_ACCESS_KEY_ID: string;
     S3_SECRET_ACCESS_KEY: string;
     S3_BUCKET_NAME: string;

    Save and close the file.

    Your edited file should look like the one below:

     namespace NodeJS {
         interface ProcessEnv {
             APP_ENV: string;
             COOKIE_SECRET: string;
             SUPERADMIN_USERNAME: string;
             SUPERADMIN_PASSWORD: string;
             DB_HOST: string;
             DB_PORT: number;
             DB_NAME: string;
             DB_USERNAME: string;
             DB_PASSWORD: string;
             DB_SCHEMA: string;
             S3_ENDPOINT: string;
             S3_ACCESS_KEY_ID: string;
             S3_SECRET_ACCESS_KEY: string;
             S3_BUCKET_NAME: string;
         }
  4. Edit the .env file:

     $ nano .env
  5. Add the following environment variables to the file. Replace the placeholder values with your actual Vultr Object Storage details

     S3_ENDPOINT=https://YOUR_VULTR_OBJECT_STORAGE_HOST
     S3_ACCESS_KEY_ID=YOUR_VULTR_OBJECT_STORAGE_ACCESS_KEY
     S3_SECRET_ACCESS_KEY=YOUR_VULTR_OBJECT_STORAGE_SECRET_KEY
     S3_BUCKET_NAME=YOUR_VULTR_OBJECT_STORAGE_BUCKET_NAME

    Save and close the file.

    You can view your Vultr Object Storage details on the instance overview section

    S3 Credentials

  6. Back up the original vendure-config.ts file

     $ mv src/vendure-config.ts src/vendure-config.ORIG
  7. Create the file again

     $ nano src/vendure-config.ts 
  8. Add the following updated contents to the file. Replace example.com with your actual domain

     import {
         dummyPaymentHandler,
         DefaultJobQueuePlugin,
         DefaultSearchPlugin,
         VendureConfig,
     } from '@vendure/core';
     import { AssetServerPlugin, configureS3AssetStorage } from '@vendure/asset-server-plugin';
     import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
     import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
     import 'dotenv/config';
     import path from 'path';
    
     const IS_DEV = process.env.APP_ENV === 'dev';
    
     export const config: VendureConfig = {
         apiOptions: {
             port: 3000,
             adminApiPath: 'admin-api',
             shopApiPath: 'shop-api',
             // The following options are useful in development mode,
             // but are best turned off for production for security
             // reasons.
             ...(IS_DEV ? {
                 adminApiPlayground: {
                     settings: { 'request.credentials': 'include' } as any,
                 },
                 adminApiDebug: true,
                 shopApiPlayground: {
                     settings: { 'request.credentials': 'include' } as any,
                 },
                 shopApiDebug: true,
             } : {}),
         },
         authOptions: {
             tokenMethod: ['bearer', 'cookie'],
             superadminCredentials: {
                 identifier: process.env.SUPERADMIN_USERNAME,
                 password: process.env.SUPERADMIN_PASSWORD,
             },
             cookieOptions: {
               secret: process.env.COOKIE_SECRET,
             },
         },
         dbConnectionOptions: {
             type: 'postgres',
             // See the README.md "Migrations" section for an explanation of
             // the `synchronize` and `migrations` options.
             synchronize: false,
             migrations: [path.join(__dirname, './migrations/*.+(js|ts)')],
             logging: false,
             database: process.env.DB_NAME,
             schema: process.env.DB_SCHEMA,
             host: process.env.DB_HOST,
             port: +process.env.DB_PORT,
             username: process.env.DB_USERNAME,
             password: process.env.DB_PASSWORD,
         },
         paymentOptions: {
             paymentMethodHandlers: [dummyPaymentHandler],
         },
         // When adding or altering custom field definitions, the database will
         // need to be updated. See the "Migrations" section in README.md.
         customFields: {},
         plugins: [
             AssetServerPlugin.init({
                 route: 'assets',
                 assetUploadDir: path.join(__dirname, '../static/assets'),
                 assetUrlPrefix: IS_DEV ? undefined : 'https://example.com/assets/',
                 storageStrategyFactory: configureS3AssetStorage({
                     bucket: process.env.S3_BUCKET_NAME,
                     credentials: {
                         accessKeyId: process.env.S3_ACCESS_KEY_ID,
                         secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
                     },
                     nativeS3Configuration: {
                         endpoint: process.env.S3_ENDPOINT,
                         forcePathStyle: true,
                         signatureVersion: 'v4',
                         region: 'eu-west-1',
                     },
                 }),
             }),
             DefaultJobQueuePlugin.init({ useDatabaseForBuffer: true }),
             DefaultSearchPlugin.init({ bufferUpdates: false, indexStockStatus: true }),
             EmailPlugin.init({
                 devMode: true,
                 outputPath: path.join(__dirname, '../static/email/test-emails'),
                 route: 'mailbox',
                 handlers: defaultEmailHandlers,
                 templatePath: path.join(__dirname, '../static/email/templates'),
                 globalTemplateVars: {
                     // The following variables will change depending on your storefront implementation.
                     // Here we are assuming a storefront running at http://localhost:8080.
                     fromAddress: '"example" <noreply@example.com>',
                     verifyEmailAddressUrl: 'http://localhost:8080/verify',
                     passwordResetUrl: 'http://localhost:8080/password-reset',
                     changeEmailAddressUrl: 'http://localhost:8080/verify-email-address-change'
                 },
             }),
             AdminUiPlugin.init({
                 route: 'admin',
                 port: 3002,
             }),
         ],
     };

    Save and close the file.

    The above configuration imports the configureS3AssetStorage and asset-server-plugin assets to Vendure. Then, it defines the Vendure URL example.com in the AssetServerPlugin.init({ section.

Store the Job Queue and Session Cache in Redis

Vendure keeps the job queue in the PostgreSQL database by default. To store the jobs in a Vultr Managed Database for Redis, use the BullMQ job queue plugin as described in the steps below.

Set Up the Job Queue

  1. Install the BullMQ job queue plugin

     $ npm install @vendure/job-queue-plugin bullmq@1 --save
  2. Edit the environment.d.ts file

     $ nano src/environment.d.ts
  3. Add the following declarations to the ProcessEnv interface after your S3 directives

     REDIS_HOST: string;
     REDIS_PORT: number;
     REDIS_USERNAME: string;
     REDIS_PASSWORD: string;

    Save and close the file.

  4. Edit the vendure-config.ts file

     $ nano src/vendure-config.ts
  5. Add the BullMQJobQueuePlugin import directive at the top of the file

     import { BullMQJobQueuePlugin } from '@vendure/job-queue-plugin/package/bullmq';

    In the plugins: section, find the DefaultJobQueuePlugin initialization directive

     DefaultJobQueuePlugin.init({ useDatabaseForBuffer: true }),

    Replace it with the following BullMQJobQueuePlugin declarations

     BullMQJobQueuePlugin.init({
         connection: {
             port: process.env.REDIS_PORT,
             host: process.env.REDIS_HOST,
             username: process.env.REDIS_USERNAME,
             password: process.env.REDIS_PASSWORD,
             tls: {},
         }
     }),

    Save and close the file.

  6. Edit the .env file

     $ nano .env
  7. Add the following environment variables at the end of the file. Replace the placeholder values with your Vultr Managed Database for Redis details

     REDIS_HOST=host.vultrd.com
     REDIS_PORT=1234
     REDIS_USERNAME=admin
     REDIS_PASSWORD=strong-password

    You can find your Vultr Managed Database for Redis details on your instance overview section

    Redis connection details

Configure Session Cache

Vendure stores the session object cache in your system memory. It's fast and suitable for a single-instance deployment. However, for horizontal scaling or multi-instance deployment, you must store the session cache to an external data store such as a Vultr Managed Database for Redis. To enable Redis, create a custom session cache strategy as described below.

  1. Create a new redis-session-cache-strategy.ts file in the plugins directory

     $ nano src/plugins/redis-session-cache-strategy.ts
  2. Add the following configurations to the file

     import { CachedSession, Logger, SessionCacheStrategy, VendurePlugin } from '@vendure/core';
     import { Redis, RedisOptions } from 'ioredis';
    
     export interface RedisSessionCachePluginOptions {
       namespace?: string;
       redisOptions?: RedisOptions;
     }
     const loggerCtx = 'RedisSessionCacheStrategy';
     const DEFAULT_NAMESPACE = 'vendure-session-cache';
    
     export class RedisSessionCacheStrategy implements SessionCacheStrategy {
       private client: Redis;
       constructor(private options: RedisSessionCachePluginOptions) {}
    
       init() {
         this.client = new Redis(this.options.redisOptions as RedisOptions);
         this.client.on('error', err => Logger.error(err.message, loggerCtx, err.stack));
       }
    
       async get(sessionToken: string): Promise<CachedSession | undefined> {
         const retrieved = await this.client.get(this.namespace(sessionToken));
         if (retrieved) {
           try {
             return JSON.parse(retrieved);
           } catch (e: any) {
             Logger.error(`Could not parse cached session data: ${e.message}`, loggerCtx);
           }
         }
       }
    
       async set(session: CachedSession) {
         await this.client.set(this.namespace(session.token), JSON.stringify(session));
       }
    
       async delete(sessionToken: string) {
         await this.client.del(this.namespace(sessionToken));
       }
    
       clear() {
         // not implemented
       }
    
       private namespace(key: string) {
         return `${this.options.namespace ?? DEFAULT_NAMESPACE}:${key}`;
       }
     }
    
     @VendurePlugin({
       configuration: config => {
         config.authOptions.sessionCacheStrategy = new RedisSessionCacheStrategy(
           RedisSessionCachePlugin.options,
         );
         return config;
       },
     })
     export class RedisSessionCachePlugin {
       static options: RedisSessionCachePluginOptions;
       static init(options: RedisSessionCachePluginOptions) {
         this.options = options;
         return this;
       }
     }

    Save and close the file

  3. Edit the vendure-config.ts file

     $ nano src/vendure-config.ts
  4. Add the RedisSessionCachePlugin directive to the import section

     import { RedisSessionCachePlugin } from './plugins/redis-session-cache-strategy';

    Within the Plugins: [ section, add the following code after AdminUiPlugin.init(...) to initialize the RedisSessionCachePlugin

     RedisSessionCachePlugin.init({
         redisOptions: {
             port: process.env.REDIS_PORT,
             host: process.env.REDIS_HOST,
             username: process.env.REDIS_USERNAME,
             password: process.env.REDIS_PASSWORD,
             tls: {},
         }
     }),

    Save and close the file.

Build for Vendure Production

  1. Edit the .env file

     $ nano .env
  2. Change the APP_ENV value from dev to production

     APP_ENV=production

    Save and close the file.

  3. Install the Vendure harden plugin that locks down your schema and protects your shop API from malicious queries

     $ npm install @vendure/harden-plugin --save
  4. Edit the vendure-config.ts file

     $ nano src/vendure-config.ts
  5. Add the following harden plugin import directive to the import section

     import { HardenPlugin } from '@vendure/harden-plugin';

    Within the Plugins: section, add the following code to initialize the harden plugin

     HardenPlugin.init({
         maxQueryComplexity: 500,
         apiMode: IS_DEV ? 'dev' : 'prod',
     }),

    Save and close the file.

    The edited vendure-config.ts file should look like the one below:

     import {
         dummyPaymentHandler,
         DefaultJobQueuePlugin,
         DefaultSearchPlugin,
         VendureConfig,
     } from '@vendure/core';
     import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
     import { AssetServerPlugin, configureS3AssetStorage } from '@vendure/asset-server-plugin';
     import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
     import { BullMQJobQueuePlugin } from '@vendure/job-queue-plugin/package/bullmq';
     import { RedisSessionCachePlugin } from './plugins/redis-session-cache-strategy';
     import { HardenPlugin } from '@vendure/harden-plugin';
     import 'dotenv/config';
     import path from 'path';
     import { readFileSync } from 'node:fs';
    
     const IS_DEV = process.env.APP_ENV === 'dev';
    
     export const config: VendureConfig = {
         apiOptions: {
             port: 3000,
             adminApiPath: 'admin-api',
             shopApiPath: 'shop-api',
             // The following options are useful in development mode,
             // but are best turned off for production for security
             // reasons.
             ...(IS_DEV ? {
                 adminApiPlayground: {
                     settings: { 'request.credentials': 'include' } as any,
                 },
                 adminApiDebug: true,
                 shopApiPlayground: {
                     settings: { 'request.credentials': 'include' } as any,
                 },
                 shopApiDebug: true,
             } : {}),
         },
         authOptions: {
             tokenMethod: ['bearer', 'cookie'],
             superadminCredentials: {
                 identifier: process.env.SUPERADMIN_USERNAME,
                 password: process.env.SUPERADMIN_PASSWORD,
             },
             cookieOptions: {
               secret: process.env.COOKIE_SECRET,
             },
         },
         dbConnectionOptions: {
             type: 'postgres',
             // See the README.md "Migrations" section for an explanation of
             // the `synchronize` and `migrations` options.
             synchronize: false,
             migrations: [path.join(__dirname, './migrations/*.+(js|ts)')],
             logging: false,
             database: process.env.DB_NAME,
             schema: process.env.DB_SCHEMA,
             host: process.env.DB_HOST,
             port: +process.env.DB_PORT,
             username: process.env.DB_USERNAME,
             password: process.env.DB_PASSWORD,
         ssl: {
                 rejectUnauthorized: true,
                 ca: readFileSync('/usr/local/share/ca-certificates/ca-certificate.crt').toString(),
             },
         },
         paymentOptions: {
             paymentMethodHandlers: [dummyPaymentHandler],
         },
         // When adding or altering custom field definitions, the database will
         // need to be updated. See the "Migrations" section in README.md.
         customFields: {},
         plugins: [
             AssetServerPlugin.init({
                 route: 'assets',
                 assetUploadDir: path.join(__dirname, '../static/assets'),
                 assetUrlPrefix: IS_DEV ? undefined : 'https://example.hisman.org/assets/',
                 storageStrategyFactory: configureS3AssetStorage({
                     bucket: process.env.S3_BUCKET_NAME,
                     credentials: {
                         accessKeyId: process.env.S3_ACCESS_KEY_ID,
                         secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
                     },
                     nativeS3Configuration: {
                         endpoint: process.env.S3_ENDPOINT,
                         forcePathStyle: true,
                         signatureVersion: 'v4',
                         region: 'eu-west-1',
                     },
                 }),
             }),
         RedisSessionCachePlugin.init({
                 redisOptions: {
                     port: process.env.REDIS_PORT,
                     host: process.env.REDIS_HOST,
                     username: process.env.REDIS_USERNAME,
                     password: process.env.REDIS_PASSWORD,
                     tls: {},
                 }
             }),
             BullMQJobQueuePlugin.init({
                 connection: {
                     port: process.env.REDIS_PORT,
                     host: process.env.REDIS_HOST,
                     username: process.env.REDIS_USERNAME,
                     password: process.env.REDIS_PASSWORD,
                     tls: {},
                 }
             }),
             HardenPlugin.init({
                 maxQueryComplexity: 500,
                 apiMode: IS_DEV ? 'dev' : 'prod',
             }),
             DefaultSearchPlugin.init({ bufferUpdates: false, indexStockStatus: true }),
             EmailPlugin.init({
                 devMode: true,
                 outputPath: path.join(__dirname, '../static/email/test-emails'),
                 route: 'mailbox',
                 handlers: defaultEmailHandlers,
                 templatePath: path.join(__dirname, '../static/email/templates'),
                 globalTemplateVars: {
                     // The following variables will change depending on your storefront implementation.
                     // Here we are assuming a storefront running at http://localhost:8080.
                     fromAddress: '"example" <noreply@example.com>',
                     verifyEmailAddressUrl: 'http://localhost:8080/verify',
                     passwordResetUrl: 'http://localhost:8080/password-reset',
                     changeEmailAddressUrl: 'http://localhost:8080/verify-email-address-change'
                 },
             }),
             AdminUiPlugin.init({
                 route: 'admin',
                 port: 3002,
             }),
         ],
     };
  6. Build the Vendure application

     $ npm run build

    npm writes the build files to the dist directory

Run the Production Vendure App

  1. Install the PM2 package

     $ sudo npm install pm2 -g
  2. Run the Vendure app server in cluster mode

     $ pm2 start ./dist/index.js -i max
  3. Run the Vendure app worker in cluster mode

     $ pm2 start ./dist/index-worker.js -i max

    Output:

     [PM2] Starting /home/user/vendure-app/dist/index-worker.js in cluster_mode (0 instance)
     [PM2] Done.
     ┌────┬─────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
     │ id │ name            │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
     ├────┼─────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
     │ 0  │ index           │ default     │ 0.1.0   │ cluster │ 232015   │ 47s    │ 0    │ online    │ 0%       │ 135.0mb  │ user     │ disabled │
     │ 1  │ index-worker    │ default     │ 0.1.0   │ cluster │ 232052   │ 0s     │ 0    │ online    │ 0%       │ 36.5mb   │ user     │ disabled │
     └────┴─────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
  4. Generate and run a startup script to start PM2 when the server reboots

     $ pm2 startup

    Output:

     [PM2] Init System found: systemd
     [PM2] To setup the Startup Script, copy/paste the following command:
     sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u example_user --hp /home/example_user
  5. Save all the running processes

     $ pm2 save
  6. Using wget, test that your Vendure app runs on the default port 3000

     $ wget -S http://localhost:3000/admin/

    Output:

     --2023-08-18 10:30:28--  http://localhost:3000/admin/
     Resolving localhost (localhost)... ::1, 127.0.0.1
     Connecting to localhost (localhost)|::1|:3000... connected.
     HTTP request sent, awaiting response...
       HTTP/1.1 200 OK
       X-Powered-By: Express
       Vary: Origin
       Access-Control-Allow-Credentials: true
       Access-Control-Expose-Headers: vendure-auth-token
       Accept-Ranges: bytes
       Cache-Control: public, max-age=0
       Last-Modified: Fri, 18 Aug 2023 09:34:01 GMT
       ETag: W/"268-18a07fe9b82"
       Content-Type: text/html; charset=UTF-8
       Content-Length: 616
       Date: Fri, 18 Aug 2023 10:30:28 GMT
       Connection: keep-alive
       Keep-Alive: timeout=5
     Length: 616 [text/html]
     Saving to: ‘index.html’

    If Vendure fails to run and listen on port 3000. Start it using the following command to view the runtime log and catch any errors

     $ npm run dev

    Output:

     [server] info 8/31/23, 8:25 PM - [BullMQJobQueuePlugin] Checking Redis connection... 
     [server] info 8/31/23, 8:25 PM - [BullMQJobQueuePlugin] Connected to Redis ✔ 
     [server] info 8/31/23, 8:25 PM - [NestApplication] Nest application successfully started 
     [server] info 8/31/23, 8:25 PM - [Vendure Server] ================================================ 
     [server] info 8/31/23, 8:25 PM - [Vendure Server] Vendure server (v2.0.6) now running on port 3000 
     [server] info 8/31/23, 8:25 PM - [Vendure Server] ------------------------------------------------ 
     [server] info 8/31/23, 8:25 PM - [Vendure Server] Shop API:     http://localhost:3000/shop-api 
     [server] info 8/31/23, 8:25 PM - [Vendure Server] Admin API:    http://localhost:3000/admin-api 
     [server] info 8/31/23, 8:25 PM - [Vendure Server] Asset server: http://localhost:3000/assets 
     [server] info 8/31/23, 8:25 PM - [Vendure Server] Dev mailbox:  http://localhost:3000/mailbox 
     [server] info 8/31/23, 8:25 PM - [Vendure Server] Admin UI:     http://localhost:3000/admin 
     [server] info 8/31/23, 8:25 PM - [Vendure Server] ================================================ 

    When successful, verify that Vendure runs on port 3000

Configure Nginx as a Reverse Proxy

To securely access the Vendure app through your domain name, configure Nginx as a reverse proxy to handle connections to the backend port 3000 as described below.

  1. Install Nginx

     $ sudo apt install nginx
  2. Disable the default Nginx configuration

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

     $ sudo nano /etc/nginx/sites-available/vendure
  4. Add the following configurations to the file. Replace example.com with your actual domain

     server {
         listen 80;
    
         server_name example.com;
    
         location / {
             proxy_pass http://localhost:3000;
             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;
         }
     }

    Save and close the file.

  5. Enable the Nginx configuration file

     $ sudo ln -s /etc/nginx/sites-available/vendure /etc/nginx/sites-enabled/
  6. Test your configurations from syntax errors

     $ sudo nginx -t
  7. Reload Nginx configurations to save changes

     $ sudo nginx -s reload

Security

To secure Vendure for production use, allow Nginx to accept incoming connections on HTTP port 80 and the HTTPS port 443. Then, securely redirect all HTTP requests to HTTPS by generating SSL certificates as described in the steps below.

Configure the Firewall

  1. Allow SSH port connection to the server

     $ sudo ufw allow 'OpenSSH'
  2. Allow the Nginx HTTP and HTTPS ports profile

     $ sudo ufw allow 'Nginx Full'
  3. Enable the firewall

     $ sudo ufw enable
  4. Verify the firewall status

     $ sudo ufw status

    Output:

     Status: active
    
     To                         Action      From
     --                         ------      ----
     22/tcp                     ALLOW       Anywhere
     Nginx Full                 ALLOW       Anywhere
     22/tcp (v6)                ALLOW       Anywhere (v6)
     Nginx Full (v6)            ALLOW       Anywhere (v6)

Secure Vendure with Let's Encrypt SSL Certificates

  1. Using snap, install the Certbot Let's Encrypt client tool

     $ sudo snap install --classic certbot
  2. Create a symbolic link for the system wide Certbot command to /usr/bin

     $ sudo ln -s /snap/bin/certbot /usr/bin/certbot
  3. Generate your Let's Encrypt SSL certificate. Replace example.com, hello@example.com with your domain, and email address respectively

     $ sudo certbot --nginx -d example.com -m hello@example.com --agree-tos
  4. Verify that Certbot auto renews the SSL certificate upon expiry

     $ sudo certbot renew --dry-run

Test the Application

  1. Using a web browser such as Chrome, access your Vendure shop administrator page

     https://example.com/admin

    Vendure login page

    Log in with the superadmin credentials you set earlier. When successful, the administrator dashboard should display

    Vendure administrator dashboard

  2. Navigate to the Inventory menu. Verify that all sample products display on the page. Try to update or delete any of them.

    Vendure inventory page

  3. Navigate to the Assets menu and upload a sample image

    Vendure assets page

  4. In your Vultr Customer Portal session, view your Vultr Object Storage bucket. Verify that the image is available in your bucket

    Vendure bucket

  5. Besides the Vendure administrator page, access the GraphQL API endpoints below:

    • Administrator GraphQL API: https://example.com/admin-api
    • Shop GraphQL API: https://example.com/shop-api

When you design your Vendure Shop, the frontend interface activates with your products and design.

Conclusion

In this guide, you installed and deployed a Vendure application to a production environment. You also configured Vendure to use Vultr Object Storage, a Vultr Managed Database for PostgreSQL, and Redis. For more information about Vendure, visit the official documentation.

More Information

For more information, visit the following resources: