0722 330 954

Your Quote Cart

Review selected architecture

    Cart is empty. Explore our services.

Migration Guide From DirectAdmin to aaPanel 2026 -2028

Share:

We spent six days migrating 100+ laravel client sites from DirectAdmin to aaPanel. The migration itself went fine less than 5mins downtime but every Laravel 12 + Livewire v4 + Flux site threw 404s for scripts. Nginx didn't recognize the asset requests. flux.js and flux.css were being treated as regular PHP routes, returning the homepage instead of the files.

Ask any AI and it will loop you. We paid for Gemini Pro, used GPT Astra, burned four hours—all fruitless. Then, during an "enough AI for today" documentation reading spree, I stared at the Nginx location matching rules and added a single pipe character.

It worked.

This article is for developers who prefer open-source tools and have stumbled onto aaPanel. If you're migrating from DirectAdmin or cPanel, or if you're being tortured by Laravel 11 + Livewire + Flux static asset 404s—this is our complete record.

Where the Problem Lives: aaPanel's Laravel Preset Is Still Stuck on Laravel 5

In aaPanel's panel, when you select an Nginx rewrite rule for a site, the dropdown gives you exactly one option: Laravel5. No Laravel 11,12 or 13. No Livewire. No Flux.

As of October 8, 2026, aaPanel's rewrite presets are still stuck in the Laravel 5 era. The Laravel 5 standard Nginx config looks like this:

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

This rule works fine for traditional Laravel apps. But it has a fatal flaw: it captures all static file requests, including /flux/flux.js, /flux/flux.css, and /livewire/livewire.js—assets that Livewire and Flux serve dynamically through routes.

When the browser requests /flux/flux.js, Nginx tries to find that file on the filesystem. It can't, because Flux assets aren't physical files—they're generated dynamically via Laravel routes. After try_files fails, the request is forwarded to index.php, Laravel returns the homepage HTML with a 200 status code.

The browser receives an HTML page expecting a JavaScript file. Every Flux component breaks.

Multiple reports on Laracasts and GitHub confirm this. Under CloudPanel, Laravel Forge, and any Nginx environment where Laravel serves assets through routes, the standard try_files rule causes the same 404-disguised-as-200 problem.

The AI Loop: Why Gemini and GPT Couldn't Save Us

We tried every major AI coding assistant. They all gave remarkably similar answers: modify the location / try_files rule, or add a separate location block for /flux/.

We tried Flux's official documentation recommendation:

nginx
location ~ ^/flux/flux(\.min)?\.(js|css)$ {
    expires off;
    try_files $uri $uri/ /index.php?$query_string;
}

Developers on GitHub reported this config still 404s under CloudPanel because CloudPanel's default config has a broader static file location block that matches .js and .css files first.

AI assistants can't solve this for you because they don't know how many competing location blocks exist in your Nginx config. They can only suggest based on "typical" configurations in their training data. Your config—especially one inherited from DirectAdmin—is not typical.

We spent four hours. Tried over a dozen config variants. Each time: modify, restart Nginx, clear cache, refresh browser. Fail, fail, fail.

Finally, I abandoned the "ask AI to write config" approach and read Nginx's official location matching documentation. Once I understood the matching priority, the problem became clear: we needed a negative lookahead assertion telling Nginx: "For requests ending in .js, .css, .woff, and similar static file extensions, unless the path starts with /flux/ or /livewire/, only then apply static file handling."

That pipe character is the "or" inside the negative lookahead.

The Solution: Negative Lookahead + Pipe Character

This is the complete Nginx location config that works in our environment. It supports Laravel 11, 12 & 13 + Livewire v4 + Flux:

nginx
# Static file handling: exclude /livewire/ and /flux/ paths
location ~* ^(?!/(livewire|flux)).*\.(?:ico|css|js|gif|jpe?g|png|svg|woff|woff2|mp4|webm)$ {
    expires max;
    log_not_found off;
    access_log off;
    try_files $uri $uri/ /index.php?$query_string;
}

The critical part is ^(?!/(livewire|flux)).

  • ^ means start of string

  • (?!...) is a negative lookahead: requires what follows to not match the pattern inside the parentheses

  • /(livewire|flux) matches paths starting with /livewire or /flux

  • .*\.(...)$ matches paths ending with the specified static file extensions

Combined, this means: If the request path does not start with /livewire/ or /flux/, and ends with .js, .css, .woff, etc., apply static file handling logic.

For /flux/flux.js, the negative lookahead fails (because the path starts with /flux), so this location block is skipped. The request falls through to Laravel's routing system, and Flux's controller returns the correct JavaScript content.

For /assets/app.css, the negative lookahead succeeds (path doesn't start with /flux or /livewire), Nginx tries to find the file on the filesystem, returns it if found, and hands it to Laravel routing if not.

This pattern matches the solution eventually accepted in GitHub's Livewire Flux issue #869. The difference is we spent four hours and dozens of dollars in AI subscriptions to "rediscover" it.

The Extra Pitfall: aaPanel's PHP Version Installation and Switching

aaPanel's PHP version management is another minefield.

When you install PHP 8.3 from the App Store, the panel may fail, or the version may not appear in site configuration after installation. Multiple forum reports confirm this.

Terminal install command:

bash
bash /www/server/panel/install/php.sh install 8.5

Note: The number must match what aaPanel's App Store displays. If the App Store shows 83, the command should be:

bash
bash /www/server/panel/install/php.sh install 83

Official aaPanel forum replies confirm this. Some versions need two digits, others three. You must verify against your panel's display.

Still unresolved: After installing a PHP version, switching versions in site config may not show the new version in the dropdown. Multiple aaPanel forum posts (2024-2026) report this. Some cases resolve with panel updates; others persist.

Additional limitation for sub-accounts: If you use aaPanel's sub-account feature, PHP 8.4 and above, once installed on the main panel, will not appear in sub-account site configuration options. This is a known limitation of aaPanel v8. Sub-panels currently do not support PHP 8.4+.

Why AI Loops on This Problem

This isn't about AI not being smart enough. It's about AI's architecture making it unable to reliably solve this class of problem.

Training data bias: AI models saw mostly standard Nginx configs during training. Standard configs don't include a dozen custom location blocks inherited from DirectAdmin, multi-layer include nesting, or interactions with aaPanel's panel logic. AI suggestions are correct in "textbook environments" and wrong in your production environment.

Context window limits: Even if you paste your complete Nginx config to an AI, it struggles to track priority competition between multiple location blocks. Nginx's matching logic (exact match > ^~ prefix > regex > plain prefix) requires a global view, while AI tends to optimize locally.

Version lag: As we covered in our earlier article about AI not being a magic bullet, AI's "knowledge cutoff" means it may not know Livewire Flux exists at all, or only knows older asset-serving patterns.

Izah Njoroge was right: AI is as smart as the one prompting it. When you don't know the concept of "negative lookahead assertions," you can't guide AI toward the correct direction in your prompts. You just spin in the AI's loop, thinking you haven't described the problem clearly enough.

What solved it after four hours wasn't a better prompt. It was reading Nginx's official documentation.

Practical Advice for Developers

If you're migrating from DirectAdmin/cPanel to aaPanel in 2026, and your stack is Laravel 11+:

1. Don't trust aaPanel's Laravel preset. It's a Laravel 5-era relic. Manually edit the site config and add the negative lookahead location block above.

2. Back up all custom Nginx configs before migrating. DirectAdmin's config structure differs from aaPanel's. Which location blocks are active and which are redundant needs re-examination after migration.

3. Never let try_files capture dynamic routes on static files. Any assets served through Laravel routes (Livewire, Flux, Filament, etc.) need to be excluded from static file handling.

4. Install PHP versions via terminal command, not the App Store. Verify the version number format against your panel display.

5. Testing checklist: Immediately after migration, check whether /flux/flux.js, /livewire/livewire.js, and /build/assets/app.css return correct MIME types and status codes.

6. About rewrite rules: aaPanel's rewrite config file path is /www/server/panel/vhost/rewrite/yourdomain.conf. Restart Nginx after modifications.

If You'd Rather Not Step on These Landmines Yourself

We spent six days migrating from direct admin, four hours of AI looping trying to solve the masked 404 error, and ksh 3,700 in subscriptions to put one pipe character in the right place. This experience is free to share. Time is not reversible.

If you're migrating to aaPanel, or stuck on Laravel 11 + Livewire + Flux static asset 404s, we can help:

  • Server migration and Nginx config audit — Complete migration from DirectAdmin/cPanel to aaPanel, including adaptation of all custom location rules

  • Laravel production environment deployment — Correct Nginx config for Livewire, Flux, Filament, and other dynamic asset frameworks

  • aaPanel troubleshooting — PHP version installation, switching issues, rewrite rule debugging

Consultation starts at Ksh 2,000.

#web development #web hosting #aapanel #Direct Admin
Share

Join the discussion 0

Your privacy is protected.

Your data will be saved securely on your device so you don't have to re-enter it next time. We will never share or sell your information.

Be the first to share your thoughts!