1(self.webpackChunkgatsby_getting_started_tutorial=self.webpackChunkgatsby_getting_started_tutorial||[]).push([[3485],{31807:function(e,n,l){"use strict";l.r(n),l.d(n,{default:function(){return E}});var t=l(15785),a=l(11151),r=l(67294);function s(e){const n=Object.assign({h2:"h2",p:"p",strong:"strong",ul:"ul",li:"li",table:"table",thead:"thead",tr:"tr",th:"th",tbody:"tbody",td:"td",a:"a",code:"code",h3:"h3",pre:"pre",blockquote:"blockquote",input:"input",hr:"hr",ol:"ol"},(0,a.ah)(),e.components);return r.createElement(r.Fragment,null,r.createElement(n.h2,{id:"why-preview-environments-for-laravel"},"Why Preview Environments for Laravel?"),"\n",r.createElement(n.p,null,"Every Laravel team knows the drill: you write a migration, test it locally against SQLite, push to staging â and it breaks because staging uses MySQL with different charset defaults. Or someone else deployed their queue worker changes to staging right before your demo. Or the Horizon dashboard shows jobs piling up because two feature branches are fighting over the same Redis instance."),"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Preview environments solve this.")," Every pull request gets its own isolated deployment â Laravel app with PHP-FPM and Nginx, MySQL database, Redis for cache and queues â running in Kubernetes with production-like configuration. Reviewers click a link and see the actual running app, not just the diff."),"\n",r.createElement(n.p,null,"With Bunnyshell, you get:"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Automatic deployment")," â A new environment spins up for every PR"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Production parity")," â Same Docker images, same database engine, same queue driver"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Isolation")," â Each PR environment is fully independent, no shared staging conflicts"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Automatic cleanup")," â Environments are destroyed when the PR is merged or closed"),"\n"),"\n",r.createElement(n.h2,{id:"choose-your-approach"},"Choose Your Approach"),"\n",r.createElement(n.p,null,"Bunnyshell supports three ways to set up preview environments for Laravel. Pick the one that fits your workflow:"),"\n",r.createElement(n.table,null,r.createElement(n.thead,null,r.createElement(n.tr,null,r.createElement(n.th,null,"Approach"),r.createElement(n.th,null,"Best for"),r.createElement(n.th,null,"Complexity"),r.createElement(n.th,null,"CI/CD maintenance"))),r.createElement(n.tbody,null,r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,r.createElement(n.a,{href:"#approach-a-bunnyshell-ui--zero-cicd-maintenance"},"Approach A: Bunnyshell UI"))),r.createElement(n.td,null,"Teams that want the fastest setup with zero pipeline maintenance"),r.createElement(n.td,null,"Easiest"),r.createElement(n.td,null,"None â Bunnyshell manages webhooks automatically")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,r.createElement(n.a,{href:"#approach-b-docker-compose-import"},"Approach B: Docker Compose Import"))),r.createElement(n.td,null,"Teams already using ",r.createElement(n.code,null,"docker-compose.yml")," for local development"),r.createElement(n.td,null,"Easy"),r.createElement(n.td,null,"None â import converts to Bunnyshell config automatically")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,r.createElement(n.a,{href:"#approach-c-helm-charts"},"Approach C: Helm Charts"))),r.createElement(n.td,null,"Teams with existing Helm infrastructure or complex K8s needs"),r.createElement(n.td,null,"Advanced"),r.createElement(n.td,null,"Optional â can use CLI or Bunnyshell UI")))),"\n",r.createElement(n.p,null,"All three approaches end the same way: ",r.createElement(n.strong,null,"a toggle in Bunnyshell Settings that enables automatic preview environments for every PR"),". No GitHub Actions, no GitLab CI pipelines to maintain â Bunnyshell adds webhooks to your Git provider and listens for PR events."),"\n",r.createElement(n.h2,{id:"prerequisites-prepare-your-laravel-app"},"Prerequisites: Prepare Your Laravel App"),"\n",r.createElement(n.p,null,"Regardless of which approach you choose, your Laravel app needs a proper Docker setup and the right configuration for running behind a Kubernetes ingress."),"\n",r.createElement(n.h3,{id:"1-create-a-production-ready-dockerfile"},"1. Create a Production-Ready Dockerfile"),"\n",r.createElement(n.p,null,"Laravel typically runs with ",r.createElement(n.strong,null,"PHP-FPM + Nginx")," in production. Here's a multi-stage Dockerfile:"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-dockerfile"},'FROM php:8.3-fpm-alpine AS base\n\n# Install system dependencies and PHP extensions\nRUN apk add --no-cache \\\n libpq-dev \\\n libzip-dev \\\n freetype-dev \\\n libjpeg-turbo-dev \\\n libpng-dev \\\n oniguruma-dev \\\n icu-dev \\\n && apk add --virtual .build-deps $PHPIZE_DEPS \\\n && pecl install redis \\\n && docker-php-ext-enable redis \\\n && docker-php-ext-configure gd --with-freetype --with-jpeg \\\n && docker-php-ext-install \\\n pdo_mysql \\\n pdo_pgsql \\\n zip \\\n gd \\\n mbstring \\\n intl \\\n opcache \\\n pcntl \\\n && apk del .build-deps\n\nWORKDIR /var/www\n\n# Install Composer\nCOPY --from=composer:2 /usr/bin/composer /usr/bin/composer\n\n# Install PHP dependencies\nCOPY composer.json composer.lock ./\nRUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist\n\n# Copy application code\nCOPY . .\n\n# Generate optimized autoloader and cache\nRUN composer dump-autoload --optimize \\\n && php artisan config:clear \\\n && php artisan route:clear \\\n && php artisan view:clear\n\n# Set permissions\nRUN chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache\n\nEXPOSE 9000\nCMD ["php-fpm"]\n')),"\n",r.createElement(n.blockquote,null,"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Important:")," Laravel with PHP-FPM listens on port ",r.createElement(n.code,null,"9000")," (FastCGI), not HTTP. You need an ",r.createElement(n.strong,null,"Nginx sidecar")," to handle HTTP requests and proxy to PHP-FPM. We'll configure this in the Bunnyshell environment."),"\n"),"\n",r.createElement(n.h3,{id:"2-create-the-nginx-configuration"},"2. Create the Nginx Configuration"),"\n",r.createElement(n.p,null,"Create ",r.createElement(n.code,null,"docker/nginx/Dockerfile"),":"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-dockerfile"},"FROM nginx:1.25-alpine\n\nCOPY default.conf /etc/nginx/conf.d/default.conf\n\nEXPOSE 8080\n")),"\n",r.createElement(n.p,null,"And ",r.createElement(n.code,null,"docker/nginx/default.conf"),":"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-nginx"},"server {\n listen 8080;\n server_name _;\n root /var/www/public;\n index index.php;\n\n client_max_body_size 50M;\n\n location / {\n try_files $uri $uri/ /index.php?$query_string;\n }\n\n location ~ \\.php$ {\n fastcgi_pass localhost:9000;\n fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;\n include fastcgi_params;\n fastcgi_buffers 16 16k;\n fastcgi_buffer_size 32k;\n }\n\n location ~ /\\.(?!well-known).* {\n deny all;\n }\n}\n")),"\n",r.createElement(n.blockquote,null,"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Key detail:")," ",r.createElement(n.code,null,"fastcgi_pass localhost:9000")," â in Kubernetes, PHP-FPM and Nginx run as ",r.createElement(n.strong,null,"sidecar containers in the same pod"),", so they share ",r.createElement(n.code,null,"localhost"),". This is different from Docker Compose where you'd use the service name."),"\n"),"\n",r.createElement(n.h3,{id:"3-configure-laravel-for-kubernetes"},"3. Configure Laravel for Kubernetes"),"\n",r.createElement(n.p,null,"Laravel needs specific settings to work correctly behind a Kubernetes ingress (which terminates TLS):"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-php"},"// bootstrap/app.php (Laravel 11+)\nreturn Application::configure(basePath: dirname(__DIR__))\n ->withMiddleware(function (Middleware $middleware) {\n // Trust all proxies â K8s ingress terminates TLS\n $middleware->trustProxies(at: '*');\n })\n ->create();\n")),"\n",r.createElement(n.p,null,"For ",r.createElement(n.strong,null,"Laravel 10 and earlier"),", edit ",r.createElement(n.code,null,"app/Http/Middleware/TrustProxies.php"),":"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-php"},"protected $proxies = '*';\nprotected $headers = Request::HEADER_X_FORWARDED_FOR |\n Request::HEADER_X_FORWARDED_HOST |\n Request::HEADER_X_FORWARDED_PORT |\n Request::HEADER_X_FORWARDED_PROTO;\n")),"\n",r.createElement(n.p,null,"Without this, Laravel generates ",r.createElement(n.code,null,"http://")," URLs instead of ",r.createElement(n.code,null,"https://"),", causing mixed-content errors, broken redirects, and infinite redirect loops."),"\n",r.createElement(n.h3,{id:"4-environment-variables"},"4. Environment Variables"),"\n",r.createElement(n.p,null,"Update your ",r.createElement(n.code,null,".env.example")," to include Bunnyshell-friendly defaults:"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-env"},"APP_KEY=\nAPP_URL=https://example.com\nAPP_ENV=production\nAPP_DEBUG=false\n\nDB_CONNECTION=mysql\nDB_HOST=mysql\nDB_PORT=3306\nDB_DATABASE=laravel\nDB_USERNAME=laravel\nDB_PASSWORD=\n\nCACHE_STORE=redis\nSESSION_DRIVER=redis\nQUEUE_CONNECTION=redis\n\nREDIS_HOST=redis\nREDIS_PORT=6379\n")),"\n",r.createElement(n.h3,{id:"laravel-deployment-checklist"},"Laravel Deployment Checklist"),"\n",r.createElement(n.ul,{className:"contains-task-list"},"\n",r.createElement(n.li,{className:"task-list-item"},r.createElement(n.input,{type:"checkbox",checked:!0,disabled:!0})," ","Dockerfile includes all required PHP extensions (",r.createElement(n.code,null,"pdo_mysql"),", ",r.createElement(n.code,null,"redis"),", ",r.createElement(n.code,null,"gd"),", ",r.createElement(n.code,null,"mbstring"),", ",r.createElement(n.code,null,"intl"),", ",r.createElement(n.code,null,"opcache"),", ",r.createElement(n.code,null,"pcntl"),")"),"\n",r.createElement(n.li,{className:"task-list-item"},r.createElement(n.input,{type:"checkbox",checked:!0,disabled:!0})," ","pecl extensions (",r.createElement(n.code,null,"redis"),") installed with ",r.createElement(n.code,null,"$PHPIZE_DEPS")),"\n",r.createElement(n.li,{className:"task-list-item"},r.createElement(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",r.createElement(n.code,null,"trustProxies")," middleware configured for K8s ingress TLS termination"),"\n",r.createElement(n.li,{className:"task-list-item"},r.createElement(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",r.createElement(n.code,null,"APP_KEY")," will be set via environment variable"),"\n",r.createElement(n.li,{className:"task-list-item"},r.createElement(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",r.createElement(n.code,null,"SESSION_DRIVER")," set to ",r.createElement(n.code,null,"redis")," (not ",r.createElement(n.code,null,"database")," â avoids needing sessions migration)"),"\n",r.createElement(n.li,{className:"task-list-item"},r.createElement(n.input,{type:"checkbox",checked:!0,disabled:!0})," ","Nginx sidecar configuration uses ",r.createElement(n.code,null,"localhost:9000")," for FastCGI"),"\n",r.createElement(n.li,{className:"task-list-item"},r.createElement(n.input,{type:"checkbox",checked:!0,disabled:!0})," ",r.createElement(n.code,null,"APP_URL")," will use Bunnyshell interpolation for dynamic URLs"),"\n",r.createElement(n.li,{className:"task-list-item"},r.createElement(n.input,{type:"checkbox",checked:!0,disabled:!0})," ","File permissions set for ",r.createElement(n.code,null,"storage/")," and ",r.createElement(n.code,null,"bootstrap/cache/")),"\n"),"\n",r.createElement(n.hr),"\n",r.createElement(n.h2,{id:"approach-a-bunnyshell-ui-zero-cicd-maintenance"},"Approach A: Bunnyshell UI â Zero CI/CD Maintenance"),"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"This is the easiest approach.")," You connect your repo, paste a YAML config, deploy, and flip a toggle. No CI/CD pipelines to write or maintain â Bunnyshell automatically adds webhooks to your Git provider and creates/destroys preview environments when PRs are opened/closed."),"\n",r.createElement(n.h3,{id:"step-1-create-a-project-and-environment"},"Step 1: Create a Project and Environment"),"\n",r.createElement(n.ol,null,"\n",r.createElement(n.li,null,"Log into ",r.createElement(n.a,{href:"https://environments.bunnyshell.com"},"Bunnyshell")),"\n",r.createElement(n.li,null,"Click ",r.createElement(n.strong,null,"Create project"),' and name it (e.g., "Laravel App")'),"\n",r.createElement(n.li,null,"Inside the project, click ",r.createElement(n.strong,null,"Create environment"),' and name it (e.g., "laravel-main")'),"\n"),"\n",r.createElement(n.h3,{id:"step-2-define-the-environment-configuration"},"Step 2: Define the Environment Configuration"),"\n",r.createElement(n.p,null,"Click ",r.createElement(n.strong,null,"Configuration")," in your environment view and paste this ",r.createElement(n.code,null,"bunnyshell.yaml"),":"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-yaml"},"kind: Environment\nname: laravel-preview\ntype: primary\n\nenvironmentVariables:\n APP_KEY: SECRET[\"base64:your-generated-app-key\"]\n DB_PASSWORD: SECRET[\"your-db-password\"]\n\ncomponents:\n # ââ Nginx
1Sidecar ââ\n - kind: SidecarContainer\n name: nginx\n gitRepo: 'https://github.com/your-org/your-laravel-repo.git'\n gitBranch: main\n gitApplicationPath: docker/nginx\n dockerCompose:\n build:\n context: docker/nginx\n ports:\n - '8080:8080'\n\n # ââ Laravel Application (PHP-FPM) ââ\n - kind: Application\n name: laravel-app\n gitRepo: 'https://github.com/your-org/your-laravel-repo.git'\n gitBranch: main\n gitApplicationPath: /\n dockerCompose:\n build:\n context: .\n dockerfile: Dockerfile\n environment:\n APP_KEY: '{{ env.vars.APP_KEY }}'\n APP_URL: 'https://{{ components.laravel-app.ingress.hosts[0] }}'\n APP_ENV: production\n APP_DEBUG: 'false'\n DB_CONNECTION: mysql\n DB_HOST: mysql\n DB_PORT: '3306'\n DB_DATABASE: laravel\n DB_USERNAME: laravel\n DB_PASSWORD: '{{ env.vars.DB_PASSWORD }}'\n CACHE_STORE: redis\n SESSION_DRIVER: redis\n QUEUE_CONNECTION: redis\n REDIS_HOST: redis\n REDIS_PORT: '6379'\n ports:\n - '9000:9000'\n - '8080:8080'\n pod:\n sidecar_containers:\n - from: nginx\n name: nginx\n shared_paths:\n - path: /var/www\n target:\n path: /var/www\n container: '@parent'\n initial_contents: '@target'\n dependsOn:\n - mysql\n - redis\n hosts:\n - hostname: 'app-{{ env.base_domain }}'\n path: /\n servicePort: 8080\n\n # ââ MySQL Database ââ\n - kind: Database\n name: mysql\n dockerCompose:\n image: 'mysql:8.0'\n environment:\n MYSQL_ROOT_PASSWORD: '{{ env.vars.DB_PASSWORD }}'\n MYSQL_DATABASE: laravel\n MYSQL_USER: laravel\n MYSQL_PASSWORD: '{{ env.vars.DB_PASSWORD }}'\n ports:\n - '3306:3306'\n\n # ââ Redis Cache & Queue ââ\n - kind: Service\n name: redis\n dockerCompose:\n image: 'redis:7-alpine'\n ports:\n - '6379:6379'\n\nvolumes:\n - name: mysql-data\n mount:\n component: mysql\n containerPath: /var/lib/mysql\n size: 1Gi\n")),"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Key architecture notes:")),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"PHP-FPM + Nginx sidecar")," â They run in the same pod, sharing the ",r.createElement(n.code,null,"/var/www")," directory. Nginx handles HTTP on port 8080, proxies PHP requests to ",r.createElement(n.code,null,"localhost:9000")," (PHP-FPM)"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,r.createElement(n.code,null,"initial_contents: '@target'"))," â Copies the built Laravel app files from the PHP-FPM container to the shared volume, so Nginx can serve static assets"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Both ports declared on the Application")," â Port ",r.createElement(n.code,null,"9000")," (PHP-FPM) and ",r.createElement(n.code,null,"8080")," (Nginx sidecar) must both be listed on the parent component because they share the pod network"),"\n"),"\n",r.createElement(n.p,null,"Replace ",r.createElement(n.code,null,"your-org/your-laravel-repo")," with your actual repository. Save the configuration."),"\n",r.createElement(n.h3,{id:"step-3-deploy"},"Step 3: Deploy"),"\n",r.createElement(n.p,null,"Click the ",r.createElement(n.strong,null,"Deploy")," button, select your Kubernetes cluster, and click ",r.createElement(n.strong,null,"Deploy Environment"),". Bunnyshell will:"),"\n",r.createElement(n.ol,null,"\n",r.createElement(n.li,null,"Build your PHP-FPM Docker image from the Dockerfile"),"\n",r.createElement(n.li,null,"Build the Nginx sidecar image from ",r.createElement(n.code,null,"docker/nginx/")),"\n",r.createElement(n.li,null,"Pull MySQL and Redis images"),"\n",r.createElement(n.li,null,"Deploy everything into an isolated Kubernetes namespace"),"\n",r.createElement(n.li,null,"Generate HTTPS URLs automatically with DNS"),"\n"),"\n",r.createElement(n.p,null,"Monitor the deployment in the environment detail page. When status shows ",r.createElement(n.strong,null,"Running"),", click ",r.createElement(n.strong,null,"Endpoints")," to access your live Laravel app."),"\n",r.createElement(n.h3,{id:"step-4-run-post-deploy-commands"},"Step 4: Run Post-Deploy Commands"),"\n",r.createElement(n.p,null,"After deployment, run Laravel setup commands via the component's terminal in the Bunnyshell UI, or via CLI:"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-bash"},"export BUNNYSHELL_TOKEN=your-api-token\nbns components list --environment ENV_ID --output json | jq '._embedded.item[] | {id, name}'\n\n# Run migrations\nbns exec COMPONENT_ID -c laravel-app -- php artisan migrate --force\n\n# Clear and rebuild caches\nbns exec COMPONENT_ID -c laravel-app -- php artisan config:cache\nbns exec COMPONENT_ID -c laravel-app -- php artisan route:cache\nbns exec COMPONENT_ID -c laravel-app -- php artisan view:cache\n\n# Link storage (for file uploads)\nbns exec COMPONENT_ID -c laravel-app -- php artisan storage:link\n")),"\n",r.createElement(n.blockquote,null,"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Important:")," Always specify ",r.createElement(n.code,null,"-c laravel-app")," to target the PHP-FPM container. Without it, ",r.createElement(n.code,null,"bns exec")," may connect to the Nginx sidecar and hang waiting for interactive input."),"\n"),"\n",r.createElement(n.h3,{id:"step-5-enable-automatic-preview-environments"},"Step 5: Enable Automatic Preview Environments"),"\n",r.createElement(n.p,null,"This is the magic step â ",r.createElement(n.strong,null,"no CI/CD configuration needed"),":"),"\n",r.createElement(n.ol,null,"\n",r.createElement(n.li,null,"In your environment, go to ",r.createElement(n.strong,null,"Settings")),"\n",r.createElement(n.li,null,"Find the ",r.createElement(n.strong,null,"Ephemeral environments")," section"),"\n",r.createElement(n.li,null,"Toggle ",r.createElement(n.strong,null,'"Create ephemeral environments on pull request"')," to ON"),"\n",r.createElement(n.li,null,"Toggle ",r.createElement(n.strong,null,'"Destroy environment after merge or close pull request"')," to ON"),"\n",r.createElement(n.li,null,"Select the Kubernetes cluster for ephemeral environments"),"\n"),"\n",r.createElement(n.p,null,"That's it. Bunnyshell automatically adds a webhook to your Git provider (GitHub, GitLab, or Bitbucket). From now on:"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Open a PR")," â Bunnyshell creates an ephemeral environment with the PR's branch"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Push to PR")," â The environment redeploys with the latest changes"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Bunnyshell posts a comment")," on the PR with a link to the live deployment"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Merge or close the PR")," â The ephemeral environment is automatically destroyed"),"\n"),"\n",r.createElement(n.blockquote,null,"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Note:")," The primary environment must be in ",r.createElement(n.strong,null,"Running")," or ",r.createElement(n.strong,null,"Stopped")," status before ephemeral environments can be created from it."),"\n"),"\n",r.createElement(n.hr),"\n",r.createElement(n.h2,{id:"approach-b-docker-compose-import"},"Approach B: Docker Compose Import"),"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Already have a ",r.createElement(n.code,null,"docker-compose.yml")," for local development?")," Most Laravel projects do. Bunnyshell can import it directly and convert it to its environment format. No manual YAML writing required."),"\n",r.createElement(n.h3,{id:"step-1-add-a-docker-composeyml-to-your-repo"},"Step 1: Add a docker-compose.yml to Your Repo"),"\n",r.createElement(n.p,null,"If you don't already have one, create ",r.createElement(n.code,null,"docker-compose.yml")," in your repo root:"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-yaml"},"version: '3.8'\n\nservices:\n laravel-app:\n build:\n context: .\n dockerfile: Dockerfile\n volumes:\n - .:/var/www\n ports:\n - '9000:9000'\n environment:\n APP_KEY: 'base64:your-key-here'\n APP_URL: 'http://localhost:8080'\n APP_ENV: local\n DB_CONNECTION: mysql\n DB_HOST: mysql\n DB_PORT: '3306'\
1n DB_DATABASE: laravel\n DB_USERNAME: laravel\n DB_PASSWORD: secret\n CACHE_STORE: redis\n SESSION_DRIVER: redis\n QUEUE_CONNECTION: redis\n REDIS_HOST: redis\n depends_on:\n - mysql\n - redis\n\n nginx:\n build:\n context: docker/nginx\n ports:\n - '8080:8080'\n volumes:\n - .:/var/www\n depends_on:\n - laravel-app\n\n mysql:\n image: mysql:8.0\n environment:\n MYSQL_ROOT_PASSWORD: secret\n MYSQL_DATABASE: laravel\n MYSQL_USER: laravel\n MYSQL_PASSWORD: secret\n volumes:\n - mysql-data:/var/lib/mysql\n ports:\n - '3306:3306'\n\n redis:\n image: redis:7-alpine\n ports:\n - '6379:6379'\n\nvolumes:\n mysql-data:\n")),"\n",r.createElement(n.h3,{id:"step-2-import-into-bunnyshell"},"Step 2: Import into Bunnyshell"),"\n",r.createElement(n.ol,null,"\n",r.createElement(n.li,null,"Create a ",r.createElement(n.strong,null,"Project")," and ",r.createElement(n.strong,null,"Environment")," in Bunnyshell (same as Approach A, Step 1)"),"\n",r.createElement(n.li,null,"Click ",r.createElement(n.strong,null,"Define environment")),"\n",r.createElement(n.li,null,"Select your Git account and repository"),"\n",r.createElement(n.li,null,"Set the branch (e.g., ",r.createElement(n.code,null,"main"),") and the path to ",r.createElement(n.code,null,"docker-compose.yml")," (use ",r.createElement(n.code,null,"/")," if it's in the root)"),"\n",r.createElement(n.li,null,"Click ",r.createElement(n.strong,null,"Continue")," â Bunnyshell parses and validates your Docker Compose file"),"\n"),"\n",r.createElement(n.p,null,"Bunnyshell automatically detects:"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,"All services (laravel-app, nginx, mysql, redis)"),"\n",r.createElement(n.li,null,"Exposed ports"),"\n",r.createElement(n.li,null,"Build configurations (Dockerfiles)"),"\n",r.createElement(n.li,null,"Volumes"),"\n",r.createElement(n.li,null,"Environment variables"),"\n"),"\n",r.createElement(n.p,null,"It converts everything into a ",r.createElement(n.code,null,"bunnyshell.yaml")," environment definition."),"\n",r.createElement(n.blockquote,null,"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Important:")," The ",r.createElement(n.code,null,"docker-compose.yml")," is only read during the initial import. Subsequent changes to the file won't auto-propagate â edit the environment configuration in Bunnyshell instead."),"\n"),"\n",r.createElement(n.h3,{id:"step-3-adjust-the-configuration"},"Step 3: Adjust the Configuration"),"\n",r.createElement(n.p,null,"After import, go to ",r.createElement(n.strong,null,"Configuration")," in the environment view and update:"),"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Replace hardcoded secrets")," with ",r.createElement(n.code,null,'SECRET["..."]')," syntax:"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-yaml"},'environmentVariables:\n APP_KEY: SECRET["base64:your-app-key"]\n DB_PASSWORD: SECRET["your-db-password"]\n')),"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Add dynamic URLs")," using Bunnyshell interpolation:"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-yaml"},"APP_URL: 'https://{{ components.laravel-app.ingress.hosts[0] }}'\n")),"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Convert Nginx to a sidecar")," â The Docker Compose import may create Nginx as a separate component. For proper Laravel operation, convert it to a sidecar container on the PHP-FPM component (see the sidecar pattern in Approach A). This ensures Nginx and PHP-FPM share the same pod and can communicate via ",r.createElement(n.code,null,"localhost:9000"),"."),"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Update Nginx config")," â Change ",r.createElement(n.code,null,"fastcgi_pass")," from the Docker Compose service name to ",r.createElement(n.code,null,"localhost:9000"),":"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-nginx"},"# Docker Compose: fastcgi_pass laravel-app:9000;\n# Bunnyshell (sidecar): fastcgi_pass localhost:9000;\n")),"\n",r.createElement(n.h3,{id:"step-4-deploy-and-enable-preview-environments"},"Step 4: Deploy and Enable Preview Environments"),"\n",r.createElement(n.p,null,"Same as Approach A â click ",r.createElement(n.strong,null,"Deploy"),", then go to ",r.createElement(n.strong,null,"Settings")," and toggle on ephemeral environments."),"\n",r.createElement(n.h3,{id:"best-practices-for-docker-compose-with-bunnyshell"},"Best Practices for Docker Compose with Bunnyshell"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Sidecar vs. separate service")," â In Docker Compose, Nginx is a separate service. In Bunnyshell (Kubernetes), Nginx should be a sidecar on the PHP-FPM pod. This avoids cross-pod FastCGI overhead and simplifies networking"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Remove local volumes")," â ",r.createElement(n.code,null,"volumes: ['.:/var/www']")," is for local dev (live reload). Remove these in Bunnyshell â the Docker image already contains the built app"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Use Bunnyshell interpolation")," for dynamic values like URLs:"),"\n"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-yaml"},"# Local docker-compose.yml\nAPP_URL: http://localhost:8080\n\n# Bunnyshell environment c
1onfig (after import)\nAPP_URL: 'https://{{ components.laravel-app.ingress.hosts[0] }}'\n")),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Design for startup resilience")," â Kubernetes doesn't guarantee ",r.createElement(n.code,null,"depends_on")," ordering. Make your Laravel app retry database connections on startup (Laravel handles this gracefully by default, but verify with your queue workers)"),"\n"),"\n",r.createElement(n.hr),"\n",r.createElement(n.h2,{id:"approach-c-helm-charts"},"Approach C: Helm Charts"),"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"For teams with existing Helm infrastructure")," or complex Kubernetes requirements (custom ingress, service mesh, advanced scaling, Horizon with auto-scaling workers). Helm gives you full control over every Kubernetes resource."),"\n",r.createElement(n.h3,{id:"step-1-create-a-helm-chart"},"Step 1: Create a Helm Chart"),"\n",r.createElement(n.p,null,"Structure your Laravel Helm chart in your repo:"),"\n",r.createElement(n.pre,null,r.createElement(n.code,null,"helm/laravel/\nâââ Chart.yaml\nâââ values.yaml\nâââ templates/\n âââ deployment.yaml\n âââ service.yaml\n âââ ingress.yaml\n âââ configmap.yaml\n âââ migration-job.yaml\n")),"\n",r.createElement(n.p,null,"A minimal ",r.createElement(n.code,null,"values.yaml"),":"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-yaml"},'replicaCount: 1\nimage:\n repository: ""\n tag: latest\nnginx:\n image:\n repository: ""\n tag: latest\nservice:\n port: 8080\ningress:\n enabled: true\n className: bns-nginx\n host: ""\nenv:\n APP_KEY: ""\n APP_URL: ""\n DB_HOST: ""\n DB_DATABASE: laravel\n DB_USERNAME: laravel\n DB_PASSWORD: ""\n REDIS_HOST: ""\n CACHE_STORE: redis\n SESSION_DRIVER: redis\n QUEUE_CONNECTION: redis\n')),"\n",r.createElement(n.h3,{id:"step-2-define-the-bunnyshell-configuration"},"Step 2: Define the Bunnyshell Configuration"),"\n",r.createElement(n.p,null,"Create a ",r.createElement(n.code,null,"bunnyshell.yaml")," using Helm components:"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-yaml"},"kind: Environment\nname: laravel-helm\ntype: primary\n\nenvironmentVariables:\n APP_KEY: SECRET[\"base64:your-app-key\"]\n DB_PASSWORD: SECRET[\"your-db-password\"]\n MYSQL_DATABASE: laravel\n MYSQL_USER: laravel\n\ncomponents:\n # ââ Docker Image Builds ââ\n - kind: DockerImage\n name: laravel-image\n context: /\n dockerfile: Dockerfile\n gitRepo: 'https://github.com/your-org/your-laravel-repo.git'\n gitBranch: main\n gitApplicationPath: /\n\n - kind: DockerImage\n name: nginx-image\n context: /docker/nginx\n dockerfile: docker/nginx/Dockerfile\n gitRepo: 'https://github.com/your-org/your-laravel-repo.git'\n gitBranch: main\n gitApplicationPath: /docker/nginx\n\n # ââ MySQL via Helm (Bitnami) ââ\n - kind: Helm\n name: mysql\n runnerImage: 'dtzar/helm-kubectl:3.8.2'\n deploy:\n - |\n cat << EOF > mysql_values.yaml\n global:\n storageClass: bns-network-sc\n auth:\n rootPassword: {{ env.vars.DB_PASSWORD }}\n database: {{ env.vars.MYSQL_DATABASE }}\n username: {{ env.vars.MYSQL_USER }}\n password: {{ env.vars.DB_PASSWORD }}\n EOF\n - 'helm repo add bitnami https://charts.bitnami.com/bitnami'\n - 'helm upgrade --install --namespace {{ env.k8s.namespace }}\n --post-renderer /bns/helpers/helm/bns_post_renderer\n -f mysql_values.yaml mysql bitnami/mysql --version 9.14.4'\n - |\n MYSQL_HOST=\"mysql.{{ env.k8s.namespace }}.svc.cluster.local\"\n destroy:\n - 'helm uninstall mysql --namespace {{ env.k8s.namespace }}'\n start:\n - 'kubectl scale --replicas=1 --namespace {{ env.k8s.namespace }}\n statefulset/mysql'\n stop:\n - 'kubectl scale --replicas=0 --namespace {{ env.k8s.namespace }}\n statefulset/mysql'\n exportVariables:\n - MYSQL_HOST\n\n # ââ Laravel App via Helm ââ\n - kind: Helm\n name: laravel-app\n runnerImage: 'dtzar/helm-kubectl:3.8.2'\n deploy:\n - |\n cat << EOF > laravel_values.yaml\n replicaCount: 1\n image:\n repository: {{ components.laravel-image.image }}\n nginx:\n image:\n repository: {{ components.nginx-image.image }}\n service:\n port: 8080\n ingress:\n enabled: true\n className: bns-nginx\n host: app-{{ env.base_domain }}\n env:\n APP_KEY: '{{ env.vars.APP_KEY }}'\n APP_URL: 'https://app-{{ env.base_domain }}'\n APP_ENV: production\n DB_CONNECTION: mysql\n DB_HOST: '{{ components.mysql.exported.MYSQL_HOST }}'\n DB_DATABASE: '{{ env.vars.MYSQL_DATABASE }}'\n DB_USERNAME: '{{ env.vars.MYSQL_USER }}'\n DB_PASSWORD: '{{ env.vars.DB_PASSWORD }}'\n CACHE_STORE: redis\n SESSION_DRIVER: redis\n QUEUE_CONNECTION: redis\n REDIS_HOST: redis\n EOF\n - 'helm upgrade --install --namespace {{ env.k8s.namespace }}\n --post-renderer /bns/helpers/helm/bns_post_renderer\n -f laravel_values.yaml laravel-{{ env.unique }} ./helm/laravel'\n destroy:\n - 'helm uninstall laravel-{{ env.unique }} --namespace {{ env.k8s.namespace }}'\n start:\n - 'helm upgrade --namespace {{ env.k8s.namespace }}\n --post-renderer /bns/helpers/helm/bns_post_renderer\n --reuse-values --set replicaCount=1 laravel-{{ env.unique }} ./helm/laravel'\n stop:\n - 'helm upgrade --namespace {{ env.k8s.namespace }}\n --post-renderer /bns/helpers/helm/bns_post_renderer\n --reuse-values --set replicaCount=0 laravel-{{ env.unique }} ./helm/laravel'\n gitRepo: 'https://github.com/your-org/your-laravel-repo.git'\n gitBranch: main\n gitApplicationPath: /helm/laravel\n\n # ââ Redis ââ\n - kind: Service\n name: redis\n dockerCompose:\n image: 'redis:7-alpine'\n ports:\n - '6379:6379'\n")),"\n",r.createElement(n.blockquote,null,"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Key:")," Always include ",r.createElement(n.code,null,"--post-renderer /bns/helpers/helm/bns_post_renderer")," in your helm commands. This adds labels s
1o Bunnyshell can track resources, show logs, and manage component lifecycle."),"\n"),"\n",r.createElement(n.h3,{id:"step-3-deploy-and-enable-preview-environments"},"Step 3: Deploy and Enable Preview Environments"),"\n",r.createElement(n.p,null,"Same flow: paste the config in ",r.createElement(n.strong,null,"Configuration"),", hit ",r.createElement(n.strong,null,"Deploy"),", then enable ephemeral environments in ",r.createElement(n.strong,null,"Settings"),"."),"\n",r.createElement(n.hr),"\n",r.createElement(n.h2,{id:"enabling-preview-environments-all-approaches"},"Enabling Preview Environments (All Approaches)"),"\n",r.createElement(n.p,null,"Regardless of which approach you used, enabling automatic preview environments is the same:"),"\n",r.createElement(n.ol,null,"\n",r.createElement(n.li,null,"Ensure your primary environment has been deployed at least once (",r.createElement(n.strong,null,"Running")," or ",r.createElement(n.strong,null,"Stopped")," status)"),"\n",r.createElement(n.li,null,"Go to ",r.createElement(n.strong,null,"Settings")," in your environment"),"\n",r.createElement(n.li,null,"Toggle ",r.createElement(n.strong,null,'"Create ephemeral environments on pull request"')," â ON"),"\n",r.createElement(n.li,null,"Toggle ",r.createElement(n.strong,null,'"Destroy environment after merge or close pull request"')," â ON"),"\n",r.createElement(n.li,null,"Select the target Kubernetes cluster"),"\n"),"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"What happens next:")),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,"Bunnyshell adds a webhook to your Git provider automatically"),"\n",r.createElement(n.li,null,"When a developer opens a PR, Bunnyshell creates an ephemeral environment cloned from the primary, using the PR's branch"),"\n",r.createElement(n.li,null,"Bunnyshell posts a comment on the PR with a direct link to the running deployment"),"\n",r.createElement(n.li,null,"When the PR is merged or closed, the ephemeral environment is automatically destroyed"),"\n"),"\n",r.createElement(n.p,null,"No GitHub Actions. No GitLab CI pipelines. No maintenance. It just works."),"\n",r.createElement(n.h3,{id:"optional-cicd-integration-via-cli"},"Optional: CI/CD Integration via CLI"),"\n",r.createElement(n.p,null,"If you prefer to control preview environments from your CI/CD pipeline (e.g., for custom migration or seed scripts), you can use the ",r.createElement(n.a,{href:"https://documentation.bunnyshell.com/docs/bunnyshell-cli"},"Bunnyshell CLI"),":"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-bash"},'# Install\nbrew install bunnyshell/tap/bunnyshell-cli\n\n# Authenticate\nexport BUNNYSHELL_TOKEN=your-api-token\n\n# Create, deploy, and run migrations in one flow\nbns environments create --from-path bunnyshell.yaml --name "pr-123" --project PROJECT_ID --k8s CLUSTER_ID\nbns environments deploy --id ENV_ID --wait\nbns exec COMPONENT_ID -c laravel-app -- php artisan migrate --force\nbns exec COMPONENT_ID -c laravel-app -- php artisan config:cache\n')),"\n",r.createElement(n.hr),"\n",r.createElement(n.h2,{id:"remote-development-and-debugging"},"Remote Development and Debugging"),"\n",r.createElement(n.p,null,"Bunnyshell makes it easy to develop and debug directly against any environment â primary or ephemeral:"),"\n",r.createElement(n.h3,{id:"port-forwarding"},"Port Forwarding"),"\n",r.createElement(n.p,null,"Connect your local tools to the remote database:"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-bash"},"# Forward MySQL to local port 13306\nbns port-forward 13306:3306 --component MYSQL_COMPONENT_ID\n\n# Connect with MySQL client, TablePlus, or any DB tool\nmysql -h 127.0.0.1 -P 13306 -u laravel -p laravel\n\n# Forward Redis to local port 16379\nbns port-forward 16379:6379 --component REDIS_COMPONENT_ID\nredis-cli -p 16379\n")),"\n",r.createElement(n.h3,{id:"execute-laravel-commands"},"Execute Laravel Commands"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-bash"},"# Migrations\nbns exec COMPONENT_ID -c laravel-app -- php artisan migrate --force\nbns exec COMPONENT_ID -c laravel-app -- php artisan migrate:status\n\n# Tinker (interactive REPL)\nbns exec COMPONENT_ID -c laravel-app -- php artisan tinker\n\n# Queue management\nbns exec COMPONENT_ID -c laravel-app -- php artisan queue:work --once\nbns exec COMPONENT_ID -c laravel-app -- php artisan horizon:status\n\n# Cache management\nbns exec COMPONENT_ID -c laravel-app -- php artisan cache:clear\nbns exec COMPONENT_ID -c laravel-app -- php artisan config:cache\nbns exec COMPONENT_ID -c laravel-app -- php artisan route:cache\n\n# Seed data\nbns exec COMPONENT_ID -c laravel-app -- php artisan db:seed\nbns exec COMPONENT_ID -c laravel-app -- php artisan db:seed --class=DemoSeeder\n\n# Storage link\nbns exec COMPONENT_ID -c laravel-app -- php artisan storage:link\n\n# Passport keys (if using Laravel Passport)\nbns exec COMPONENT_ID -c laravel-app -- php artisan passport:keys\n")),"\n",r.createElement(n.blockquote,null,"\n",r.createElement(n.p,null,r.createElement(n.strong,null,"Always use ",r.createElement(n.code,null,"-c laravel-app"))," to target the PHP-FPM container. Without ",r.createElement(n.code,null,"-c"),", ",r.createElement(n.code,null,"bns exec")," may connect to the Nginx sidecar, which doesn't have PHP installed."),"\n"),"\n",r.createElement(n.h3,{id:"live-logs"},"Live Logs"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-bash"},"# Stream logs in real time\nbns logs --component COMPONENT_ID -f\n\n# Last 200 lines\nbns logs --component COMPONENT_ID --tail 200\n\n# Logs from the last 5 minutes\nbns logs --component COMPONENT_ID --since 5m\n")),"\n",r.createElement(n.h3,{id:"live-code-sync"},"Live Code Sync"),"\n",r.createElement(n.p,null,"For active development, sync your local code
1changes to the remote container in real time:"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-bash"},"bns remote-development up --component COMPONENT_ID\n# Edit files locally â changes sync automatically\n# When done:\nbns remote-development down\n")),"\n",r.createElement(n.p,null,"This is especially useful for debugging issues that only reproduce in the Kubernetes environment â you get the fast feedback loop of local development with the infrastructure of production."),"\n",r.createElement(n.hr),"\n",r.createElement(n.h2,{id:"advanced-queue-workers-and-horizon"},"Advanced: Queue Workers and Horizon"),"\n",r.createElement(n.p,null,"For production-grade Laravel apps, you likely need ",r.createElement(n.strong,null,"queue workers"),". Add a worker component to your ",r.createElement(n.code,null,"bunnyshell.yaml"),":"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-yaml"}," # ââ Queue Worker ââ\n - kind: Service\n name: queue-worker\n gitRepo: 'https://github.com/your-org/your-laravel-repo.git'\n gitBranch: main\n gitApplicationPath: /\n dockerCompose:\n build:\n context: .\n dockerfile: Dockerfile\n command: ['php', 'artisan', 'queue:work', '--sleep=3', '--tries=3', '--max-time=3600']\n environment:\n APP_KEY: '{{ env.vars.APP_KEY }}'\n DB_CONNECTION: mysql\n DB_HOST: mysql\n DB_DATABASE: laravel\n DB_USERNAME: laravel\n DB_PASSWORD: '{{ env.vars.DB_PASSWORD }}'\n CACHE_STORE: redis\n SESSION_DRIVER: redis\n QUEUE_CONNECTION: redis\n REDIS_HOST: redis\n dependsOn:\n - mysql\n - redis\n")),"\n",r.createElement(n.p,null,"For ",r.createElement(n.strong,null,"Laravel Horizon"),", replace the command:"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-yaml"},"command: ['php', 'artisan', 'horizon']\n")),"\n",r.createElement(n.hr),"\n",r.createElement(n.h2,{id:"advanced-task-scheduling-cron"},"Advanced: Task Scheduling (Cron)"),"\n",r.createElement(n.p,null,"If your Laravel app uses ",r.createElement(n.code,null,"Schedule")," (task scheduling), add a cron job to the PHP-FPM component:"),"\n",r.createElement(n.pre,null,r.createElement(n.code,{className:"language-yaml"}," - kind: Application\n name: laravel-app\n # ... existing config ...\n cronJobs:\n - name: laravel-scheduler\n schedule: '* * * * *'\n command: ['php', '/var/www/artisan', 'schedule:run']\n containers: [laravel-app]\n")),"\n",r.createElement(n.hr),"\n",r.createElement(n.h2,{id:"troubleshooting"},"Troubleshooting"),"\n",r.createElement(n.table,null,r.createElement(n.thead,null,r.createElement(n.tr,null,r.createElement(n.th,null,"Issue"),r.createElement(n.th,null,"Solution"))),r.createElement(n.tbody,null,r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"502 Bad Gateway")),r.createElement(n.td,null,"PHP-FPM isn't running, or Nginx can't reach it. Check that ",r.createElement(n.code,null,"fastcgi_pass")," uses ",r.createElement(n.code,null,"localhost:9000")," (not a service name). Verify both ports (",r.createElement(n.code,null,"9000")," and ",r.createElement(n.code,null,"8080"),") are declared on the Application component.")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"Mixed content / HTTPS errors")),r.createElement(n.td,null,"Missing ",r.createElement(n.code,null,"trustProxies")," middleware. Add ",r.createElement(n.code,null,"$middleware->trustProxies(at: '*')")," in ",r.createElement(n.code,null,"bootstrap/app.php"),".")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,'"The page has expired" (419 CSRF)')),r.createElement(n.td,null,r.createElement(n.code,null,"APP_URL")," doesn't match the actual URL. Use interpolation: ",r.createElement(n.code,null,"'https://{{ components.laravel-app.ingress.hosts[0] }}'"))),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"Blank page / 500 error")),r.createElement(n.td,null,"Check ",r.createElement(n.code,null,"APP_KEY")," is set. Generate with ",r.createElement(n.code,null,"php artisan key:generate --show")," and add as ",r.createElement(n.code,null,'SECRET["base64:..."]'),". Also check ",r.createElement(n.code,null,"storage/")," permissions.")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"Static assets not loading")),r.createElement(n.td,null,"Nginx sidecar doesn't have the Laravel files. Verify ",r.createElement(n.code,null,"shared_paths")," with ",r.createElement(n.code,null,"initial_contents: '@target'")," copies files from PHP-FPM to Nginx.")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"Session not persisting")),r.createElement(n.td,null,r.createElement(n.code,null,"SESSION_DRIVER")," set to ",r.createElement(n.code,null,"database")," but sessions table doesn't exist. Use ",r.createElement(n.code,null,"redis")," or ",r.createElement(n.code,null,"file")," instead.")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"Queue jobs not processing")),r.createElement(n.td,null,"Missing queue worker component. Add a separate Service component with ",r.createElement(n.code,null,"php artisan queue:work"),". Ensure Redis is reachable.")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"Migrations fail")),r.createElement(n.td,null,"Check ",r.createElement(n.code,null,"DB_HOST")," points to ",r.createElement(n.code,null,"mysql")," (the component name), not ",r.createElement(n.code,null,"localhost"),". Verify MySQL is running before running migrations.")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"Connection refused to MySQL")),r.createElement(n.td,null,"MySQL container not ready yet. Wait for the environment status to show ",r.createElement(n.strong,null,"Running")," before executing commands.")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"Passport keys error")),r.createElement(n.td,null,"Auto-generate keys in entrypoint: ",r.createElement(n.code,null,"php artisan passport:keys")," with file permissions ",r.createElement(n.code,null,"660")," owned by ",r.createElement(n.code,null,"www-data"),".")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"522 Connection timed out")),r.createElement(n.td,null,"Cluster may be behind a firewall. Verify Cloudflare IPs are whitelisted on the ingress controller.")))),"\n",r.createElement(n.h2,{id:"whats-next"},"What's Next?"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Add Laravel Horizon")," â Monitor and manage your queue workers with a dedicated dashboard"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Add Mailpit")," â Test email sending with a local SMTP server (",r.createElement(n.code,null,"axllent/mailpit")," as a Service component)"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Seed test data")," â Run ",r.createElement(n.code,null,"bns exec <ID> -c laravel-app -- php artisan db:seed --class=DemoSeeder")," post-deploy"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Add MinIO")," â S3-compatible object storage for file uploads (",r.createElement(n.code,null,"minio/minio")," as a Service component)"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Monitor with Telescope")," â Add ",r.createElement(n.code,null,"TELESCOPE_ENABLED=true")," for request/query debugging in preview environments"),"\n"),"\n",r.createElement(n.h2,{id:"related-resources"},"Related Resources"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"https://documentation.bunnyshell.com/docs/quickstart-overview"},"Bunnyshell Quickstart Guide")),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"https://documentation.bunnyshell.com/docs/quickstart-dockercompose-overview"},"Docker Compose with Bunnyshell")),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"https://documentation.bunnyshell.com/docs/quickstart-helm-overview"},"Helm with Bunnyshell")),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"https://documentation.bunnyshell.com/docs/bunnyshell-cli"},"Bunnyshell CLI Reference")),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/guides/preview-environments-for-django/"},"Preview Environments for Django")," â Same pattern for Python/Django"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/comparisons/laravel-cloud-alternative/"},"Laravel Cloud Alternatives")," â For teams whose stack is Laravel plus other services"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/blog/laravel-forge-preview-environments/"},"Laravel Forge and Preview Environments")," â The gap Forge doesn't cover, and how to close it"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/blog/laravel-vapor-limitations/"},"Laravel Vapor Limitations")," â Ephemeral filesystem, execution limits, and when Vapor isn't the right fit"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/blog/laravel-dusk-preview-environments/"},"Laravel Dusk and Preview Environments")," â Pointing your existing Dusk suite at a real per-PR environment"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/ephemeral-environments/"},"Ephemeral Environments")," â Learn more about the concept"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/guides/who-broke-staging/"},"Who Broke Staging?")," â Why shared staging environments fail"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/guides/"},"All Guides")," â More technical guides"),"\n"))}var o=function(e){void 0===e&&(e={});const{wrapper:n}=Object.assign({},(0,a.ah)(),e.components);return n?r.createElement(n,e,r.createElement(s,e)):s(e)}
1,i=l(11883),c=l(82179),u=l(13438),m=l(79629);const p={Callout:c.UW,ArticleCTA:c.hL,pre:u.Z};function d(e){const n=[];for(const l of m.ecosystems)l.guide&&l.guide.href===e&&n.push(l);return n}const h=e=>{let{guideUrl:n}=e;const l=d(n);return 0===l.length?null:r.createElement("section",{className:"ga-comparison-links"},r.createElement("h2",{id:"comparisons"},"Related Comparisons"),l.map((e=>r.createElement("div",{key:e.id,className:"ga-comparison-links__eco"},r.createElement("h3",null,e.label),r.createElement("p",{style:{color:"var(--hp2-gray-400)",fontSize:14,marginBottom:16}},e.description),r.createElement("div",{className:"ga-comparison-links__grid"},r.createElement("div",null,r.createElement("div",{className:"ga-comparison-links__col-label"},"Alternatives"),r.createElement("ul",{className:"ga-comparison-links__list"},e.hubs.slice(0,5).map((e=>r.createElement("li",{key:e.slug},r.createElement(i.Link,{to:"/comparisons/"+e.slug+"/"},e.label)))))),r.createElement("div",null,r.createElement("div",{className:"ga-comparison-links__col-label"},"Head-to-Head"),r.createElement("ul",{className:"ga-comparison-links__list"},e.vsPages.slice(0,5).map((e=>r.createElement("li",{key:e.slug},r.createElement(i.Link,{to:"/comparisons/"+e.slug+"/"},"Bunnyshell ",e.label))))))),r.createElement(i.Link,{to:"/comparisons/",className:"ga-comparison-links__all"},"View all comparisons â")))))},g=e=>{let{data:n,children:l}=e;const{mdx:s}=n,o=s.frontmatter,i=o.faqJson?JSON.parse(o.faqJson):[],u=o.relatedLinksJson?JSON.parse(o.relatedLinksJson):[],m=o.howToJson?JSON.parse(o.howToJson):null,g=function(e){if(!e||!e.items)return[];const n=[];for(const l of e.items)if(l.url&&l.title&&n.push({id:l.url.replace("#",""),text:l.title,level:2}),l.items)for(const e of l.items)e.url&&e.title&&n.push({id:e.url.replace("#",""),text:e.title,level:3});return n}(s.tableOfContents),E=function(e,n){const l="https://www.bunnyshell.com"+e.url,t=[];return t.push(Object.assign({"@context":"https://schema.org","@type":"Article",headline:e.title,description:e.description,author:{"@type":"Organization",name:"Bunnyshell",url:"https://www.bunnyshell.com/"},publisher:{"@type":"Organization",name:"Bunnyshell",url:"https://www.bunnyshell.com/",logo:{"@type":"ImageObject",url:"https://www.bunnyshell.com/images/og-image.png"}},datePublished:e.publishDate?e.publishDate+"T00:00:00+00:00":void 0,dateModified:e.modifiedDate||e.publishDate?(e.modifiedDate||e.publishDate)+"T00:00:00+00:00":void 0,mainEntityOfPage:{"@type":"WebPage","@id":l}},e.heroImage?{image:"https://www.bunnyshell.com"+e.heroImage}:{})),t.push({"@context":"https://schema.org","@type":"BreadcrumbList",itemListElement:[{"@type":"ListItem",position:1,name:"Home",item:"https://www.bunnyshell.com/"},{"@type":"ListItem",position:2,name:"Guides",item:"https://www.bunnyshell.com/guides/"},{"@type":"ListItem",position:3,name:e.title,item:l}]}),n&&n.length>0&&t.push({"@context":"https://schema.org","@type":"FAQPage",mainEntity:n.map((e=>{let{q:n,a:l}=e;return{"@type":"Question",name:n,acceptedAnswer:{"@type":"Answer",text:l}}}))}),t}(o,i),v=(n.clusterGuides?n.clusterGuides.nodes:[]).map((e=>({label:e.frontmatter.title,href:e.frontmatter.url,active:e.frontmatter.url===o.url}))),b=i.length>0?r.createElement("section",{style:{marginTop:48}},r.createElement("h2",{id:"faq"},"Frequently Asked Questions"),i.map(((e,n)=>{let{q:l,a:t}=e;return r.createElement("div",{key:n,style:{marginBottom:24}},r.createElement("h3",{id:"faq-"+(n+1)},l),r.createElement("p",null,t))}))):null,y=d(o.url).length>0,f=[].concat((0,t.Z)(g),(0,t.Z)(y?[{id:"comparisons",text:"Related Comparisons",level:2}]:[]),(0,t.Z)(i.length>0?[{id:"faq",text:"FAQ",level:2}]:[]));return r.createElement(c.ZP,{title:o.title,metaTitle:o.metaTitle?o.metaTitle+" | Bunnyshell":void 0,subtitle:o.subtitle,description:o.metaDescription||o.description,url:o.url,heroImage:o.heroImage,ogImage:o.ogImage||"https://www.bunnyshell.com/images/og-image.png",category:o.category||"Guide",publishDate:o.publishDate,modifiedDate:o.modifiedDate,readingTime:o.readingTime||"8 min read",author:{name:o.authorName||"Bunnyshell",avatar:o.authorAvatar},sidebarLinks:v,sidebarLabel:o.sidebarLabel||"Guides",relatedLinks:u,relatedLabel:o.relatedLabel||"Related",headings:f,variant:o.variant||"dark",schemaMarkup:E[1],articleSchema:JSON.stringify(E[0])},E.length>2&&r.createElement("script",{type:"application/ld+json",dangerouslySetInnerHTML:{__html:JSON.stringify(E[2])}}),m&&r.createElement("script",{type:"application/ld+json",dangerouslySetInnerHTML:{__html:JSON.stringify(m)}}),r.createElement(a.Zo,{components:p},l),r.createElement(h,{guideUrl:o.url}),b)};function E(e){return r.createElement(g,e,r.createElement(o,e))}},79629:function(e,n,l){var t=l(861);const a=[{id:"ci-cd",label:"CI/CD Platforms",description:"Continuous integration and delivery tools. Bunnyshell adds environment lifecycle management on top of your CI/CD pipeline.",guide:{label:"Agentic Development",href:"/guides/agentic-development/"},hubs:[{slug:"jenkins-alternatives",label:"Jenkins Alternatives"},{slug:"circleci-alternatives",label:"CircleCI Alternatives"},{slug:"github-actions-alternatives",label:"GitHub Actions Alternatives"},{slug:"gitlab-alternatives",label:"GitLab Alternatives"}
1,{slug:"bitbucket-alternatives",label:"Bitbucket Alternatives"},{slug:"buildkite-alternatives",label:"Buildkite Alternatives"},{slug:"teamcity-alternatives",label:"TeamCity Alternatives"},{slug:"codefresh-alternatives",label:"Codefresh Alternatives"},{slug:"jenkins-x-alternatives",label:"Jenkins X Alternatives"},{slug:"azure-devops-alternatives",label:"Azure DevOps Alternatives"}],vsPages:[{slug:"bunnyshell-vs-jenkins",label:"vs Jenkins"},{slug:"bunnyshell-vs-jenkinsx",label:"vs Jenkins X"},{slug:"bunnyshell-vs-circleci",label:"vs CircleCI"},{slug:"bunnyshell-vs-github-actions",label:"vs GitHub Actions"},{slug:"bunnyshell-vs-github",label:"vs GitHub"},{slug:"bunnyshell-vs-gitlab",label:"vs GitLab"},{slug:"bunnyshell-vs-bitbucket",label:"vs Bitbucket"},{slug:"bunnyshell-vs-buildkite",label:"vs Buildkite"},{slug:"bunnyshell-vs-teamcity",label:"vs TeamCity"},{slug:"bunnyshell-vs-codefresh",label:"vs Codefresh"},{slug:"bunnyshell-vs-travis-ci",label:"vs Travis CI"},{slug:"bunnyshell-vs-semaphore",label:"vs Semaphore"},{slug:"bunnyshell-vs-azure-devops",label:"vs Azure DevOps"},{slug:"bunnyshell-vs-jfrog",label:"vs JFrog"},{slug:"bunnyshell-vs-gitea",label:"vs Gitea"}]},{id:"gitops-deploy",label:"GitOps & Deployment",description:"GitOps controllers and deployment orchestration tools. Bunnyshell manages the full environment lifecycle, not just deployments.",guide:{label:"Who Broke Staging?",href:"/blog/who-broke-staging/"},hubs:[{slug:"argocd-alternatives",label:"Argo CD Alternatives"},{slug:"fluxcd-alternatives",label:"Flux CD Alternatives"},{slug:"spinnaker-alternatives",label:"Spinnaker Alternatives"},{slug:"harness-alternatives",label:"Harness Alternatives"},{slug:"octopus-deploy-alternatives",label:"Octopus Deploy Alternatives"},{slug:"spacelift-alternatives",label:"Spacelift Alternatives"}],vsPages:[{slug:"bunnyshell-vs-argocd",label:"vs Argo CD"},{slug:"bunnyshell-vs-fluxcd",label:"vs Flux CD"},{slug:"bunnyshell-vs-spinnaker",label:"vs Spinnaker"},{slug:"bunnyshell-vs-harness",label:"vs Harness"},{slug:"bunnyshell-vs-octopus-deploy",label:"vs Octopus Deploy"},{slug:"bunnyshell-vs-spacelift",label:"vs Spacelift"}]},{id:"paas-hosting",label:"PaaS & Hosting",description:"Platform-as-a-Service and app hosting platforms. Bunnyshell gives you full-stack environments on your own infrastructure.",guide:{label:"Who Broke Staging?",href:"/blog/who-broke-staging/"},hubs:[{slug:"heroku-alternatives",label:"Heroku Alternatives"},{slug:"render-alternatives",label:"Render Alternatives"},{slug:"railway-alternatives",label:"Railway Alternatives"},{slug:"vercel-alternatives",label:"Vercel Alternatives"},{slug:"flyio-alternatives",label:"Fly.io Alternatives"},{slug:"coolify-alternatives",label:"Coolify Alternatives"},{slug:"northflank-alternatives",label:"Northflank Alternatives"},{slug:"digitalocean-app-platform-alternatives",label:"DigitalOcean App Platform Alternatives"},{slug:"netlify-alternatives",label:"Netlify Alternatives"},{slug:"laravel-cloud-alternative",label:"Laravel Cloud Alternatives"}],vsPages:[{slug:"bunnyshell-vs-heroku",label:"vs Heroku"},{slug:"bunnyshell-vs-render",label:"vs Render"},{slug:"bunnyshell-vs-railway",label:"vs Railway"},{slug:"bunnyshell-vs-vercel",label:"vs Vercel"},{slug:"bunnyshell-vs-flyio",label:"vs Fly.io"},{slug:"bunnyshell-vs-coolify",label:"vs Coolify"},{slug:"bunnyshell-vs-northflank",label:"vs Northflank"},{slug:"bunnyshell-vs-digitalocean-app-platform",label:"vs DigitalOcean App Platform"}]}
1,{id:"dev-environments",label:"Dev Environments & Workspaces",description:"Cloud development environments and remote workspaces. Bunnyshell offers full-stack environments with AI sandbox isolation.",guide:{label:"Coding Agent Sandbox",href:"/guides/coding-agent-sandbox/"},hubs:[{slug:"gitpod-alternatives",label:"Gitpod Alternatives"},{slug:"github-codespaces-alternative",label:"GitHub Codespaces Alternatives"},{slug:"coder-alternatives",label:"Coder Alternatives"},{slug:"devpod-alternatives",label:"DevPod Alternatives"},{slug:"devbox-alternatives",label:"Devbox Alternatives"},{slug:"cloud9-alternatives",label:"Cloud9 Alternatives"},{slug:"codeanywhere-alternatives",label:"Codeanywhere Alternatives"},{slug:"daytona-alternatives",label:"Daytona Alternatives"},{slug:"eclipse-che-alternatives",label:"Eclipse Che Alternatives"},{slug:"jetbrains-space-alternatives",label:"JetBrains Space Alternatives"},{slug:"docker-desktop-alternative",label:"Docker Desktop Alternatives"},{slug:"portainer-alternatives",label:"Portainer Alternatives"}],vsPages:[{slug:"bunnyshell-vs-gitpod",label:"vs Gitpod"},{slug:"bunnyshell-vs-github-codespaces",label:"vs GitHub Codespaces"},{slug:"bunnyshell-vs-coder",label:"vs Coder"},{slug:"bunnyshell-vs-devpod",label:"vs DevPod"},{slug:"bunnyshell-vs-devbox",label:"vs Devbox"},{slug:"bunnyshell-vs-cloud9",label:"vs Cloud9"},{slug:"bunnyshell-vs-codeanywhere",label:"vs Codeanywhere"},{slug:"bunnyshell-vs-daytona",label:"vs Daytona"},{slug:"bunnyshell-vs-eclipse-che",label:"vs Eclipse Che"},{slug:"bunnyshell-vs-jetbrains-space",label:"vs JetBrains Space"}]},{id:"preview-env",label:"Preview & Ephemeral Environments",description:"Preview environment platforms and ephemeral infrastructure. Bunnyshell is purpose-built for full-stack preview environments.",guide:{label:"Who Broke Staging?",href:"/blog/who-broke-staging/"},hubs:[{slug:"okteto-alternatives",label:"Okteto Alternatives"},{slug:"qovery-alternatives",label:"Qovery Alternatives"},{slug:"release-alternatives",label:"Release Alternatives"},{slug:"signadot-alternatives",label:"Signadot Alternatives"},{slug:"vcluster-alternatives",label:"vCluster Alternatives"}],vsPages:[{slug:"bunnyshell-vs-okteto",label:"vs Okteto"},{slug:"bunnyshell-vs-qovery",label:"vs Qovery"},{slug:"bunnyshell-vs-release",label:"vs Release"},{slug:"bunnyshell-vs-signadot",label:"vs Signadot"},{slug:"bunnyshell-vs-vcluster",label:"vs vCluster"},{slug:"bunnyshell-vs-diy",label:"vs DIY"}]},{id:"platform-eng",label:"Platform Engineering",description:"Internal developer platforms and platform engineering tools. Bunnyshell provides environment self-service without building a custom IDP.",guide:{label:"IDP Failures",href:"/guides/internal-developer-platform-failures/"},hubs:[{slug:"humanitec-alternatives",label:"Humanitec Alternatives"}],vsPages:[{slug:"bunnyshell-vs-humanitec",label:"vs Humanitec"},{slug:"bunnyshell-vs-aws-proton",label:"vs AWS Proton"}]}];function r(e){for(const n of a){if([].concat(t(n.hubs.map((e=>e.slug))),t(n.vsPages.map((e=>e.slug)))).includes(e))return n}return null}e.exports={ecosystems:a,getEcosystemForSlug:r,getRelatedPages:function(e,n){void 0===n&&(n=6);const l=r(e);if(!l)return{ecosystem:null,hubs:[],vsPages:[],guide:null};const t=l.hubs.some((n=>n.slug===e)),a=l.hubs.filter((n=>n.slug!==e)).slice(0,t?n:3),s=l.vsPages.filter((n=>n.slug!==e)).slice(0,t?n:3);return{ecosystem:l,hubs:a,vsPages:s,guide:l.guide}},guideClusters:[{id:"ai-coding",label:"AI & Coding Agents",guides:[{slug:"agentic-development",label:"Agentic Development",href:"/guides/agentic-development/"},{slug:"coding-agent-sandbox",label:"Coding Agent Sandbox",href:"/guides/coding-agent-sandbox/"},{slug:"sandboxed-environments-ai-coding",label:"Sandboxed Environments for AI",href:"/guides/sandboxed-environments-ai-coding/"}]},{id:"preview-envs-backend",label:"Preview Environments: Backend Frameworks",guides:[]},{id:"preview-envs-frontend",label:"Preview Environments: Frontend Frameworks",guides:[]},{id:"preview-envs-infra",label:"Preview Environments: Infrastructure",guides:[]},{id:"preview-envs-cicd",label:"Preview Environments: CI/CD Integration",guides:[]},{id:"preview-envs-databases",label:"Preview Environments: Databases",guides:[]},{id:"aws-proton-migration",label:"AWS Proton Migration",guides:[{slug:"migrate-from-aws-proton",label:"Migration Overview",href:"/guides/migrate-from-aws-proton/"},{slug:"aws-proton-cloudformation-to-bunnyshell",label:"CloudFormation Path",href:"/guides/aws-proton-cloudformation-to-bunnyshell/"},{slug:"aws-proton-terraform-to-bunnyshell",label:"Terraform Path",href:"/guides/aws-proton-terraform-to-bunnyshell/"}]},{id:"platform-engineering",label:"Platform Engineering",guides:[{slug:"internal-developer-platform-failures",label:"Why IDPs Fail",href:"/guides/internal-developer-platform-failures/"}]},{id:"e2e-testing-automation",label:"End-to-End Testing Automation",guides:[]},{id:"staging-testing-environments",label:"Staging & Testing Environments",guides:[]},{id:"environment-lifecycle",label:"Environment Lifecycle Management",guides:[]}]}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.