Self-Host Troubleshooting Guide
A comprehensive manual mapping root causes, immediate hot fixes, and permanent remedies for common infrastructure issues encountered during self-hosting.
CORS Policy / Preflight Request Blocked
// SYMPTOM
Access to fetch at 'https://api.yourdomain.com/api/auth/login' from origin 'https://app.yourdomain.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present.
// ROOT_CAUSE
In standard bash, $# is a special variable representing the number of script arguments. In older installers, the .env generator heredoc was unescaped, causing bash to evaluate $# to 0. This wrote CORS_ALLOWED_ORIGIN_PATTERNS=#^https?://.*0 to your config, breaking regular expression matching.
// IMMEDIATE_HOT_FIX
Run this on your server host to repair the config regex and recreate the API container with updated variables:
sed -i 's|CORS_ALLOWED_ORIGIN_PATTERNS=#^https?://.*0|CORS_ALLOWED_ORIGIN_PATTERNS=#^https?://.*\$#|g' ~/docker/nusaas/.env docker compose up -d
Browser Connects to Stale/Old Domains
// SYMPTOM
You updated the domain settings in .env.frontend and recreated containers, but browser developer console still shows requests targeting old domains (e.g. wss://api.olddomain.com).
// ROOT_CAUSE
1. Running docker compose restart only restarts container processes; it does not recreate them with new environment variables.
2. The React Frontend uses a Service Worker (sw.js) that aggressively caches JS files locally in the client browser, forcing it to keep loading the old config.
// SOLUTION_STEPS
- Force recreate the containers to inject the new env values:
docker compose up -d
- Test in an Incognito / Private Window (bypasses Service Worker cache).
- In your main window, open DevTools (F12), navigate to the Network tab, check Disable Cache, and reload with Ctrl+F5 (Windows) or Cmd+Shift+R (Mac).
- If using Firefox, unregister the service worker directly at:
about:debugging#/runtime/this-firefox
Cloudflare ERR_SSL_VERSION_OR_CIPHER_MISMATCH
// SYMPTOM
This site can’t provide a secure connection. app.erp.yourdomain.com uses an unsupported protocol.
ERR_SSL_VERSION_OR_CIPHER_MISMATCH
// ROOT_CAUSE
Cloudflare's free Universal SSL certificate only covers wildcards up to the 3rd level (*.yourdomain.com). If you deploy using nested 4th-level subdomains (like app.erp.yourdomain.com), Cloudflare cannot negotiate a secure SSL handshake at the Edge unless you purchase their Advanced Certificate Manager (ACM).
// REMEDY
Flatten your subdomain structure to the 3rd level. Remove the nested erp. segment entirely:
- Use
app.yourdomain.comorapp-erp.yourdomain.com - Use
api.yourdomain.comorapi-erp.yourdomain.com
Database Migration & Seeding Hangs
// SYMPTOM
The installation script gets stuck indefinitely at:
[NuSaaS] Running database migrations and seeding... [/]
// ROOT_CAUSE
When running migrations, Laravel shells out to the native mysql binary inside the backend container to load the schema dump file. If your backend container's default MySQL client (Debian's MariaDB client) connects to a MySQL 8.0 server advertising self-signed SSL certs, the handshake fails/hangs with ERROR 2026 (HY000): TLS/SSL error.
// IMMEDIATE_HOT_FIX
Write a MariaDB client configuration directly inside the running backend container to skip SSL validation, then manually run the migration command:
docker compose exec api sh -c "printf '[client]\nskip-ssl\n' > /etc/my.cnf" docker compose exec -T api php artisan migrate --seed --force
Relational Driver Error (PostgreSQL)
// SYMPTOM
could not find driver (Connection: pgsql, Host: nusaas-db, Port: 5432, Database: nusaas ...)
// ROOT_CAUSE
Older releases of the FrankenPHP backend base image (ghostdev1/nusaas-backend) were compiled without the native PostgreSQL client binaries and PHP extension (pdo_pgsql).
// RESOLUTION
Pull down the latest v1.0.4+ images which include compiled PostgreSQL extensions, and recreate the stack:
docker compose pull docker compose up -d
Worker Crash Loops & Unhealthy Statuses
// SYMPTOM
nusaas-worker Restarting (2) 59 seconds ago
nusaas-ws Up 30 minutes (unhealthy)
nusaas-cron Up 30 minutes (unhealthy)
// ROOT_CAUSE
1. The worker container command runs supervisord on a config located at /etc/supervisor/conf.d/supervisord.conf which does not exist (the correct path inside the container is /var/www/docker/supervisor/supervisord.conf).
2. The cron and ws containers inherit the image-level healthcheck on port 4000. Since they do not run a web server, their health checks fail even though they are running properly.
// REMEDY
Modify the service configurations in your docker-compose.yml:
- Update the worker service command:
command: /usr/bin/supervisord -n -c /var/www/docker/supervisor/supervisord.conf
- Add
healthcheck: disable: trueto theworker,cron, andwsservices. - Recreate the containers:
docker compose up -d
NuSaaS Diagnostics Suite
// 1. CHECKING_CONTAINER_HEALTH
Query container statuses, check if their internal health checks are passing, and ping the API route directly from the server:
# Get docker containers status & health indicators docker compose ps # Check the API health endpoint directly inside the API container docker compose exec api curl -sf http://localhost:4000/api/health # Verify actual HTTP response headers from the API server docker compose exec api curl -i http://localhost:4000/api/health
// 2. LIVE_LOG_STREAMING
Stream logs in real-time to intercept errors during database connection failures, mail queue issues, or application crashes:
# Stream logs from all running containers docker compose logs -f # Stream only backend API container logs docker compose logs -f api # Stream queue worker logs docker compose logs -f worker # Stream MySQL/PostgreSQL logs docker compose logs -f db
// 3. CONTAINER_SHELLS_AND_DATABASE_DIAGNOSTICS
Access container environments directly or trigger internal database pingers to verify connection integrity:
# Open an interactive shell inside the API container docker compose exec -it api bash # Monitor DB connection stats in Laravel docker compose exec api php artisan db:monitor # Directly test DB connection PDO instantiation docker compose exec api php artisan tinker --execute="DB::connection()->getPdo();" # Get current system stats (RAM/CPU usage per container) docker stats --no-stream