← Documentation home Deployment Guide

Deploying Xgenious Accounting

Production setup for VPS, shared hosting, AWS and DigitalOcean.

1. Before you deploy

Server requirements

  • PHP 8.4+ with: mbstring, xml/dom, curl, zip, bcmath, intl, gd, and pdo_mysql (or pdo_sqlite).
  • Composer 2 and Node.js 20+ (build step only).
  • MySQL 8 or MariaDB 10.6+.
  • Ability to run a queue worker and a cron entry.

Environment file

Start from .env.example and set production values:

APP_NAME="Xgenious Accounting"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://app.yourdomain.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=xgenious
DB_USERNAME=xgenious
DB_PASSWORD=strong-secret

SESSION_DRIVER=database
CACHE_STORE=database
QUEUE_CONNECTION=database

MAIL_MAILER=smtp
MAIL_HOST=email-smtp.eu-west-1.amazonaws.com   # or your SMTP provider
MAIL_PORT=587
MAIL_USERNAME=...
MAIL_PASSWORD=...
MAIL_FROM_ADDRESS="billing@yourdomain.com"
MAIL_FROM_NAME="Xgenious Accounting"

# SMS notifications (driver: log, twilio, vonage, http, null)
SMS_DRIVER=log
SMS_FROM="Xgenious Accounting"
# SMS_HTTP_URL=https://sms-provider.example/api/send
# SMS_HTTP_TOKEN=...
# TWILIO_SID= / TWILIO_TOKEN= / TWILIO_FROM=
# VONAGE_KEY= / VONAGE_SECRET= / VONAGE_FROM=

Common deploy steps (all environments)

composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan key:generate --force          # only on first deploy
php artisan migrate --force
php artisan db:seed --class=AdminSeeder --force
php artisan db:seed --class=SiteSettingsSeeder --force
# optional: php artisan db:seed --class=DemoDataSeeder --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan storage:link
Where files are stored Uploads (business logo, expense receipts) are written to the private disk at storage/app/private, outside the web root, and are only served through authorized routes. This works on every host with no extra storage service.
Online payments (Stripe) Configured per business in Settings → Online payments (secret key, webhook signing secret, deposit account). The platform must be reachable over HTTPS so Stripe can POST to the webhook at /portal/webhooks/stripe. No server-wide Stripe credentials are needed.
Never point the web root at the project root The web root must be the public/ directory, otherwise .env and application files could be exposed.

2. VPS (Ubuntu 22.04 + Nginx + PHP-FPM + MySQL)

2.1 Install packages

sudo apt update
sudo apt install -y nginx mysql-server unzip git supervisor certbot python3-certbot-nginx \
  php8.4-fpm php8.4-mysql php8.4-mbstring php8.4-xml php8.4-curl php8.4-zip \
  php8.4-bcmath php8.4-intl php8.4-gd

# Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

2.2 Database

sudo mysql
CREATE DATABASE xgenious CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'xgenious'@'localhost' IDENTIFIED BY 'strong-secret';
GRANT ALL PRIVILEGES ON xgenious.* TO 'xgenious'@'localhost';
FLUSH PRIVILEGES; EXIT;

2.3 Deploy the code

sudo mkdir -p /var/www/app && sudo chown $USER /var/www/app
git clone <your-repo> /var/www/app
cd /var/www/app
cp .env.example .env
# edit .env with production values
composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan key:generate
php artisan migrate --force
php artisan db:seed --class=AdminSeeder --force

2.4 Permissions

sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache

2.5 Nginx site

Create /etc/nginx/sites-available/app:

server {
    listen 80;
    server_name app.yourdomain.com;
    root /var/www/app/public;
    index index.php;

    client_max_body_size 20M;

    location / { try_files $uri $uri/ /index.php?$query_string; }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        fastcgi_hide_header X-Powered-By;
    }

    location ~ /\.(?!well-known) { deny all; }
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
        expires 1y; add_header Cache-Control "public, immutable"; try_files $uri =404;
    }

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/app
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

2.6 SSL

sudo certbot --nginx -d app.yourdomain.com

2.7 Queue worker (Supervisor)

Create /etc/supervisor/conf.d/laravel-queue.conf:

[program:laravel-queue]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work database --sleep=3 --tries=3 --timeout=90
directory=/var/www/app
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/supervisor/laravel-queue.log
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-queue:*

2.8 Scheduler (cron)

sudo crontab -u www-data -e
# add:
* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1
One-shot installer The repository ships deploy/install.sh, deploy/nginx.conf and deploy/supervisor.conf that automate most of the above on a fresh Ubuntu 22.04 host. Review them before running.

3. Shared hosting (cPanel)

Shared hosting works if your plan provides PHP 8.4+, MySQL, cron and (ideally) a terminal. It does not support long-running processes, so queues run from cron.

3.1 Build assets locally

If Node is unavailable on the host, build on your machine and upload the result:

npm ci && npm run build

Upload the project (including public/build, vendor if you cannot run Composer on the host, and composer.lock).

3.2 Create the database

In cPanel → MySQL Databases, create a database and user, then grant all privileges. Note the full names (usually prefixed, e.g. acct_xgenious).

3.3 Point the domain at public/

Preferred: set the domain's document root to .../public (cPanel → Domains). If your host forces public_html as the docroot, either:

  • Put the app in a folder outside public_html and set the domain root to its public folder, or
  • Copy everything, then move the contents of public/ into public_html/ and edit public_html/index.php so the two require paths point at ../<app-folder>/vendor/autoload.php and ../<app-folder>/bootstrap/app.php.

3.4 Configure and install

cp .env.example .env      # then edit DB_*, APP_URL, MAIL_*
php artisan key:generate
php artisan migrate --force
php artisan db:seed --class=AdminSeeder --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan storage:link

If there is no terminal, run these via a temporary SSH session, the cPanel Terminal, or ask support. Set storage and bootstrap/cache permissions to 775.

3.5 Cron for scheduler and queue

cPanel → Cron Jobs. Add:

* * * * * /usr/local/bin/php /home/USER/app/artisan schedule:run >> /dev/null 2>&1
* * * * * /usr/local/bin/php /home/USER/app/artisan queue:work --stop-when-empty --tries=3 >> /dev/null 2>&1

The --stop-when-empty worker drains the queue each minute and exits, which is the accepted pattern on hosts that prohibit daemons.

Limitations No Redis/Supervisor guarantees, lower limits on long-running jobs and file uploads. For anything beyond a light load, prefer a VPS or cloud host below.

4. AWS (EC2 + RDS + S3 + SES)

Recommended managed topology:

  • EC2 (Ubuntu 22.04) running Nginx + PHP-FPM (app + queue), behind an Application Load Balancer with ACM for TLS.
  • RDS MySQL 8 for the database (Multi-AZ for production).
  • ElastiCache Redis (optional) for cache/queue/sessions at scale.
  • S3 for stored files (optional, see below), SES for email, CloudFront for static assets (optional).
  • SSM Parameter Store / Secrets Manager for credentials.

4.1 EC2 setup

Follow the VPS steps for Nginx/PHP-FPM. Attach an IAM role (for S3/SES) rather than static keys.

4.2 RDS

Create a MySQL 8 instance in the same VPC. In .env:

DB_CONNECTION=mysql
DB_HOST=your-db.abc123.eu-west-1.rds.amazonaws.com
DB_DATABASE=xgenious
DB_USERNAME=admin
DB_PASSWORD=...

Allow port 3306 only from the app's security group.

4.3 Redis (optional)

CACHE_STORE=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=your-cache.abc123.0001.euw1.cache.amazonaws.com
REDIS_PORT=6379

4.4 File storage on S3

By default uploads use the private local disk. To store them on S3, create a bucket and set the local disk (used by the app) to the S3 driver in config/filesystems.php, or switch the app to the default disk and set:

FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=...            # or use the instance IAM role
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=eu-west-1
AWS_BUCKET=your-xgenious-bucket
AWS_USE_PATH_STYLE_ENDPOINT=false
Keep the bucket private Files are served through authorized routes, so the bucket does not need public access.

4.5 Email with SES

MAIL_MAILER=smtp
MAIL_HOST=email-smtp.eu-west-1.amazonaws.com
MAIL_PORT=587
MAIL_USERNAME=...
MAIL_PASSWORD=...
MAIL_FROM_ADDRESS="billing@yourdomain.com"

Verify your sending domain (SPF/DKIM) in SES and request production access.

4.6 Queues on AWS

Run queue:work under Supervisor on the app instance, or on a separate worker instance/ASG. For containerized setups, run the same image as an ECS worker service with a scheduler sidecar (`php artisan schedule:work`).

4.7 Scheduler on AWS

Either a cron on an instance or an EventBridge scheduled rule invoking php artisan schedule:run each minute.

5. DigitalOcean

Option A — Droplet (simplest)

  1. Create an Ubuntu 22.04 Droplet.
  2. Follow the VPS steps exactly.
  3. Optionally attach a Managed MySQL and Managed Redis cluster and point DB_* / REDIS_* at them (add the Droplet to the clusters' trusted sources).
  4. Point your domain at the Droplet and run Certbot for SSL.

Option B — App Platform

  • Deploy from a Git repository. Set the build command to composer install --no-dev --optimize-autoloader && npm ci && npm run build and the run command to php artisan migrate --force && php-fpm (or a custom start script serving public/).
  • Add a Managed MySQL and, optionally, Managed Redis component; platforms inject the connection env vars.
  • Add a Worker component running php artisan queue:work.
  • Add a scheduled job (* * * * *) running php artisan schedule:run.
  • Set health check path to /up.

Spaces (S3-compatible) for files

FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=nyc3
AWS_BUCKET=your-space
AWS_ENDPOINT=https://nyc3.digitaloceanspaces.com
AWS_USE_PATH_STYLE_ENDPOINT=false

Use the same S3 disk changes described in the AWS section. DigitalOcean also provides email relay; otherwise use any SMTP provider in MAIL_*.

6. SSL & security

  • HTTPS everywhere. Use Certbot (VPS/Droplet) or ACM/Let's Encrypt (load balancers). Redirect HTTP → HTTPS.
  • Hide versions. fastcgi_hide_header X-Powered-By, and set expose_php = Off in php.ini.
  • Security headers (see the Nginx block): X-Frame-Options, X-Content-Type-Options, Referrer-Policy. Add HSTS once HTTPS is confirmed.
  • Set APP_DEBUG=false and APP_ENV=production.
  • Restrict database access to the app's network.
  • Keep storage/ and bootstrap/cache/ writable but not web-accessible.
  • Rotate the default admin password immediately after the first login (/admin/login → profile).

7. Queues & scheduler

Queued work: invoice emails, reminders, statements. Scheduled work: reminders, recurring expenses, housekeeping.

EnvironmentQueue workerScheduler
VPS / DropletSupervisor daemoncron schedule:run
Shared hostingcron queue:work --stop-when-emptycron schedule:run
AWSSupervisor / ECS workerEventBridge → schedule:run
App PlatformWorker componentScheduled job
# verify a worker is processing
php artisan queue:work --once
php artisan queue:failed
php artisan queue:retry all

8. Backups & restore

Database

# backup
mysqldump -u xgenious -p xgenious | gzip > /backups/xgenious-$(date +%F).sql.gz

# restore
gunzip < /backups/xgenious-2026-01-01.sql.gz | mysql -u xgenious -p xgenious

Schedule this nightly with cron and keep copies off-host (S3/Spaces). The deploy/install.sh script installs a nightly backup cron and prunes backups older than 14 days.

Files

Back up storage/app/private (uploads) and, if used, your S3/Space bucket (enable versioning). public/build is reproducible from source.

Per-tenant export

Businesses can download their own data (ZIP of CSVs) from Settings → Business → data export, independent of your infrastructure backups.

9. Updating the app

cd /var/www/app
php artisan down                       # maintenance mode (optional)

git pull --ff-only
composer install --no-dev --optimize-autoloader
npm ci && npm run build

php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart              # tell workers to reload new code

sudo supervisorctl restart laravel-queue:*
php artisan up
Zero-downtime Use a symlinked release directory (releases/<timestamp> + current symlink) with an atomic switch, or a platform with built-in zero-downtime deploys (App Platform, ECS rolling updates).

10. Troubleshooting

SymptomFix
500 error after deployCheck storage/logs/laravel.log; usually permissions on storage/bootstrap/cache or a missing APP_KEY.
Blank page / assets 404Run npm run build; ensure the web root is public/.
Config not updatingRun php artisan config:clear then re-cache.
Emails not sendingVerify MAIL_*, run php artisan mail:test you@example.com, and confirm a queue worker is running.
Reminders/overdue not updatingCheck the schedule:run cron and that the queue worker runs.
Old page after updateHard refresh; the assets are versioned by build hash.
Database connection refusedCheck credentials, host allowlist/security group, and that the DB is running.
Uploads failIncrease upload_max_filesize/post_max_size and Nginx client_max_body_size.