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, andpdo_mysql(orpdo_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
storage/app/private, outside the web root,
and are only served through authorized routes. This works on every host with no
extra storage service.
/portal/webhooks/stripe. No server-wide Stripe credentials are needed.
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
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.
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
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)
- Create an Ubuntu 22.04 Droplet.
- Follow the VPS steps exactly.
- Optionally attach a Managed MySQL and Managed Redis
cluster and point
DB_*/REDIS_*at them (add the Droplet to the clusters' trusted sources). - 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 buildand the run command tophp artisan migrate --force && php-fpm(or a custom start script servingpublic/). - 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 (
* * * * *) runningphp 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 setexpose_php = Offinphp.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=falseandAPP_ENV=production. - Restrict database access to the app's network.
- Keep
storage/andbootstrap/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.
| Environment | Queue worker | Scheduler |
|---|---|---|
| VPS / Droplet | Supervisor daemon | cron schedule:run |
| Shared hosting | cron queue:work --stop-when-empty | cron schedule:run |
| AWS | Supervisor / ECS worker | EventBridge → schedule:run |
| App Platform | Worker component | Scheduled 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
releases/<timestamp> + current symlink) with an atomic
switch, or a platform with built-in zero-downtime deploys (App Platform, ECS rolling
updates).
10. Troubleshooting
| Symptom | Fix |
|---|---|
| 500 error after deploy | Check storage/logs/laravel.log; usually permissions on storage/bootstrap/cache or a missing APP_KEY. |
| Blank page / assets 404 | Run npm run build; ensure the web root is public/. |
| Config not updating | Run php artisan config:clear then re-cache. |
| Emails not sending | Verify MAIL_*, run php artisan mail:test you@example.com, and confirm a queue worker is running. |
| Reminders/overdue not updating | Check the schedule:run cron and that the queue worker runs. |
| Old page after update | Hard refresh; the assets are versioned by build hash. |
| Database connection refused | Check credentials, host allowlist/security group, and that the DB is running. |
| Uploads fail | Increase upload_max_filesize/post_max_size and Nginx client_max_body_size. |