BlogGuides

How to self-host Bagisto without Docker on your own VPS

Run Bagisto v2.5.0 on your own server with ox: Laravel, MariaDB, a queue worker, the scheduler and a first admin made on deploy, from the config that ran.

On this page

To self-host Bagisto you need an Ubuntu server, your own fork of the Bagisto repo with three files added, and 16 variables, six of them for the database ox sets up. In our run the first deploy finished in about 2 minutes, and ox set up MariaDB, a queue worker and a timer for Laravel's scheduler.

How we tested On Oct 17, 2026, ox deployed Bagisto v2.5.0 from commit 4e7f0862 on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory. The run then redeployed it, backed it up and rebooted the server. Then a separate test agent, one that did not write the config, signed in, added a product in the admin and saw it on the storefront with its price and an Add To Cart button, and checked the app's services and its backup.

Bagisto is an open-source online store built on Laravel and Vue.js by Webkul, under the MIT license. Shoppers use the storefront at the site's address. The shop owner uses the admin panel at /admin. This guide covers the single store with the default theme.

ox deployed Bagisto v2.5.0 (commit 4e7f0862) on Oct 17, 2026 on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory; the first deploy took 2 minutes and a redeploy took 38 seconds.

  • First deploy: 2 minutes, from adding the project until the app passed the run's first checks.
  • Redeploy of the same commit: 38 seconds, until the app passed the checks again.
  • Server: DigitalOcean, 8 vCPUs and 16 GB of memory (s-8vcpu-16gb), with this app alone on it. Every one of the 85 apps we deployed ran on this one size, so it was not picked for this app.
  • Memory in use after the reboot: 898 MB of 15986 MB, for the whole server: Ubuntu, the app, its services, Caddy and the ox agent. On a server with no apps, ox's tests on real servers hold the agent to 40 MB or less and Caddy to 50 MB or less.
  • Services ox runs for it: mysql.
  • Background processes: queue, each its own systemd unit beside the web process.
  • Scheduled jobs: schedule.
  • Source: github.com/bagisto/bagisto at v2.5.0, commit 4e7f0862.
The Bagisto storefront home page with its default Velocity theme
The Bagisto storefront home page with its default Velocity theme
The Bagisto admin dashboard after signing in
The Bagisto admin dashboard after signing in
The storefront page of the new product Ox Verify Mug at $19.99 with an Add To Cart button
The storefront page of the new product Ox Verify Mug at $19.99 with an Add To Cart button

What you need

  • An Ubuntu LTS server. The run used the size in the facts above and measured the memory there. Bagisto 2.5 asks for PHP 8.4 or newer, and ox serves PHP apps on PHP 8.5.

  • A GitHub account to fork bagisto/bagisto, and an ox account, which you open by signing in with GitHub.

  • An email address such as [email protected] and a password for the first admin. Bagisto sends order and welcome mail through SMTP, and the run pointed it at a test mail catcher, so real mail was not tested.

Checkout, orders, online payments and product image uploads were not tested either.

The steps

Open the ox dashboard, press Add a server, and run the command it prints in a root shell on the new server. It sets up the ox agent and Caddy.

Fork bagisto/bagisto and check out the tag v2.5.0, commit 4e7f0862, the release the run deployed. Add ox.toml, ox-install.php and php.ini at the root of the fork, as shown below, then commit and push. Write bagisto.env on your laptop with one KEY=value line for each key in the variables section. Laravel's APP_KEY is base64: and 32 random bytes in base64. Set APP_URL to ${PUBLIC_URL}. The example file also lists keys for features this setup does not use, such as AWS and R2 storage, Reverb and Pusher. ox asks for each one, so the run marked them Not needed with unset KEY lines, and ox review stops and names any you leave out. With the ox CLI signed in, run:

Shell
ox new <you>/bagisto
ox review bagisto --from-file bagisto.env --wait

The review saves your values and starts the first deploy, and --wait prints the log until it ends. The quickstart shows the same two steps in the browser.

When the deploy is green, open the project's address plus /admin/login and sign in with ADMIN_EMAIL and ADMIN_PASSWORD. The store starts with no products. Open Catalog, Products, Create Product, and pick the Simple type, the Default attribute family and a SKU. Bagisto requires a name, URL key, short description, description, price and weight. Switch on Status and Visible Individually and save. The test agent then loaded /<url key> on the storefront and saw the product with its price and an Add To Cart button. Prices come through the queue, so a new product can take a few seconds to appear.

The run served Bagisto on its preview address only. A domain of your own was not part of it, and the domains page covers how ox adds one.

The ox.toml, line by line

TOML
# Bagisto 2.5 (Laravel 13, MySQL) on ox: FrankenPHP serves public/ (detected),
# MariaDB, the queue worker and the Laravel scheduler.
required_variables = ["ADMIN_EMAIL", "ADMIN_PASSWORD"]

[build]
# The release's own production image installs Composer's packages only: the
# storefront, admin and installer builds are committed under public/, and
# the root package.json has no lockfile. --ignore-platform-req=php: ox
# serves PHP 8.5, Bagisto requires ^8.4, and the locked
# phpoffice/phpspreadsheet caps PHP below 8.5.
install = "composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-req=php"
commands = [
  # What artisan storage:link makes: product and theme images under
  # storage/app/public served at /storage.
  "ln -sfn ../storage/app/public public/storage",
  # Bagisto counts itself installed only when a .env file exists (its
  # settings come from ox's environment, so the file stays empty); without
  # it a lost storage/installed marker would reopen the web installer.
  "touch .env",
]
# bagisto:install wipes the database every time it runs, so ox-install.php
# does its first-run steps once instead: see that file.
migrate = "php artisan migrate --force && php ox-install.php"

[workers]
# Product price and stock indexes, catalog rules, imports and mail go
# through Laravel's queue, the database one by default (config/queue.php).
queue = { run = "exec php artisan queue:work --sleep=3 --tries=3 --max-time=3600", stop_timeout = "5m" }

[cron]
# Laravel's scheduler: the daily price and catalog-rule indexes, invoice
# reminders, campaigns and exchange rates.
schedule = { schedule = "* * * * *", run = "php artisan schedule:run" }

[services]
mysql = {}

[storage]
# Laravel writes storage/: uploads, the install marker, logs, compiled views.
keep = ["storage"]

There is no [app] section: ox sees composer.json and artisan, starts FrankenPHP on public, and takes the health page /up from bootstrap/app.php.

  • required_variables names the admin's email and password. Bagisto never reads them, ox-install.php does, and the review asks for them first.

  • install runs Composer without dev packages and runs no npm, because the storefront and admin builds are committed under public. --ignore-platform-req=php is there because the locked phpoffice/phpspreadsheet 1.30.6 accepts PHP below 8.5 and ox serves 8.5, so Composer stops without it. The run's checks passed with it, and ox's docs say the app may break on a PHP it does not list.

  • The first build command links public/storage to storage/app/public, as artisan storage:link does, so images are served at /storage. touch .env makes an empty file, because Bagisto's install check needs one to exist while its settings come from ox's environment. Without it, a lost storage/installed marker would send visitors back to the web installer.

  • migrate runs Laravel's migrations, then ox-install.php. It leaves out php artisan bagisto:install, which Bagisto's own production image runs, because that command wipes the database first.

  • The queue worker runs queue:work. Bagisto queues price and stock index updates, catalog rules, imports and mail, and config/queue.php keeps that queue in the database. --max-time=3600 ends the worker after an hour, and ox's service starts it again. stop_timeout gives a running job five minutes to finish when the worker stops.

  • The schedule job runs php artisan schedule:run every minute, and Laravel decides what is due. Bagisto schedules daily price and catalog rule indexes, invoice overdue reminders, marketing campaigns, exchange rates and price snapshots every 15 minutes.

  • mysql = {} asks for a MariaDB that belongs to this project alone.

  • keep = ["storage"] names the folder Laravel writes while it runs: uploads, logs, compiled views and the install marker. A PHP release is read-only, so ox links storage to the project's data folder, which stays writable across deploys.

ox-install.php

Code
<?php

// The deploy's first-run step, after `php artisan migrate --force`. It does
// once what `php artisan bagisto:install -n` does after its migrations
// (seed the store's base data, create the first admin, mark the install
// done), but never its db:wipe, so every later deploy keeps the store. The
// admin comes from ADMIN_EMAIL and ADMIN_PASSWORD instead of the
// installer's fixed [email protected] / admin123. The web root is public/,
// so this file is never served.

use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
use Webkul\Installer\Database\Seeders\DatabaseSeeder;
use Webkul\Installer\Helpers\DatabaseManager;

if (PHP_SAPI !== 'cli') {
    exit(1);
}

require __DIR__.'/vendor/autoload.php';

$app = require __DIR__.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();

if (DB::table('admins')->exists()) {
    echo "Bagisto is already installed\n";
} else {
    $email = (string) getenv('ADMIN_EMAIL');
    $password = (string) getenv('ADMIN_PASSWORD');

    if ($email === '' || $password === '') {
        fwrite(STDERR, "ADMIN_EMAIL and ADMIN_PASSWORD must be set for the first deploy\n");
        exit(1);
    }

    // One transaction: a seed that stops part way leaves nothing behind, and
    // the next deploy starts the first run again.
    DB::transaction(function () use ($email, $password) {
        // The installer's own seed configuration (Installer::getSeederConfiguration).
        $locale = config('app.locale');
        $currency = config('app.currency');

        app(DatabaseSeeder::class)->run([
            'default_locale' => $locale,
            'allowed_locales' => [$locale],
            'default_currency' => $currency,
            'allowed_currencies' => [$currency],
            'skip_admin_creation' => true,
        ]);

        // createAdminUser reports its error to the log and returns false.
        if (! app(DatabaseManager::class)->createAdminUser([
            'name' => 'Admin',
            'email' => $email,
            'password' => $password,
        ])) {
            throw new RuntimeException('the first admin was not created: see the error above');
        }
    });

    echo "Bagisto installed: base data seeded, admin {$email} created\n";
}

// The marker Bagisto's installer writes last: with it, the installer's
// middleware takes the store as installed without asking the database.
touch(storage_path('installed')) || exit(1);

php.ini

Code
; PHP reads php.ini from the folder it starts in, the release. ox sets the
; upload limits ([app] max_upload); these two are what Bagisto's own
; production image (docker/production/shared/php.ini) sets for the web: a
; product photo resized with GD and a large import or invoice PDF outgrow
; PHP's 128M and 30 seconds.
memory_limit = 512M
max_execution_time = 300

ox-install.php does once what Bagisto's installer does after its migrations. When the admins table already has a row, it prints that Bagisto is installed and skips the seeding. Otherwise it seeds the base data in one transaction, with the app's own locale and currency, and creates the admin from ADMIN_EMAIL and ADMIN_PASSWORD. A seed that stops part way leaves nothing behind, and the next deploy starts again. Last, it writes storage/installed, the marker Bagisto's installer writes. It runs only from the command line, and the web root is public, so nobody can fetch it.

php.ini sets memory_limit to 512M and max_execution_time to 300 seconds, the numbers in Bagisto's own production php.ini, since resizing a photo or reading a large import can pass PHP's defaults. ox sets the upload limits itself.

Shell
ADMIN_EMAIL
ADMIN_PASSWORD
APP_KEY
APP_URL
DB_CONNECTION
DB_HOST
DB_PORT
DB_DATABASE
DB_USERNAME
DB_PASSWORD
LOG_CHANNEL
MAIL_HOST
MAIL_PORT
MAIL_FROM_ADDRESS
ADMIN_MAIL_ADDRESS
CONTACT_MAIL_ADDRESS

ADMIN_EMAIL and ADMIN_PASSWORD are the first admin. APP_KEY is Laravel's encryption key. APP_URL is the address the app builds links from, and ox's ${PUBLIC_URL} gives it (variables explains references). LOG_CHANNEL set to stderr puts Laravel's log in the project's logs, which ox logs bagisto reads. MAIL_HOST and MAIL_PORT name the SMTP server, a test mail catcher in the run. MAIL_FROM_ADDRESS sends every mail, ADMIN_MAIL_ADDRESS gets order notifications, and CONTACT_MAIL_ADDRESS shows in mail footers. DB_CONNECTION is mysql, and the other five DB_ keys are references to the project's database, such as ${mysql.host} (configuration page).

The database, the queue and the scheduler

The database is MariaDB 11.8, which ox calls mysql. It runs for this project alone. After the test agent saved its product, the queue's log showed the price index job finish, and the scheduler's log showed a price snapshot job finish.

Updates and backups

To move to a new release, merge its tag into your fork and push, or run ox deploy bagisto --wait. ox builds the new release in its own folder, runs migrate, and switches only when the new one answers. Then ox-install.php finds the admin and changes nothing. Bagisto's UPGRADE.md lists the high impact changes of an upgrade, so read it first. The rollback page covers a release that goes wrong.

Backups need no setup. The run's backup, made with Back up now, held a MariaDB archive and an archive of the kept storage folder. Run ox backups bagisto to list them, or ox backups bagisto now mysql to make one. A restore was not tested. The backups page covers copies in an S3 bucket.

Frequently asked questions

Do I need Docker to self-host Bagisto?

No. Bagisto publishes Docker images and a compose file, but the store is a Laravel app that runs without them. ox installs its packages with Composer on your server, serves it with FrankenPHP and adds a MariaDB service beside it. To update, push to your fork or run one deploy command.

How do I create the first Bagisto admin account?

Put ADMIN_EMAIL and ADMIN_PASSWORD in your variables before the first deploy. The migrate line runs ox-install.php, which seeds the store and creates that admin, and you sign in at /admin/login. Bagisto's installer defaults to [email protected] with the password admin123, which this setup never creates.

Which database does Bagisto use?

Bagisto's Docker images pair it with MySQL 8.0, MariaDB 10.11 or PostgreSQL 16. The run used the mysql service ox adds to the project, which is MariaDB 11.8, and ox fills the DB_ keys from it. PostgreSQL was not tested here.

How much memory does self-hosted Bagisto use?

After the reboot, the whole server in the run used the memory the facts show. That counts MariaDB, the queue worker, Caddy, the ox agent and the run's test mail catcher as well as Bagisto. ox works on any server from 1 GB up and names memory when memory is why an app stopped.

New to running apps on your own server? How ox self-hosts your apps shows what ox sets up on your server.

Keep reading

Deploy to a server you own

ox sets up systemd, Caddy and PostgreSQL on your Ubuntu server and deploys on every push. Free for your first project.

Sign up with GitHub Read the docs