Blueprint Anatomy
.larakube.json is the one file that describes your whole project. LaraKube CLI reads it during larakube up, heal, add, env, and the cloud commands β there's no hidden database and no separate config to keep in sync. Change this file, and you change how your app is deployed.
Don't let a big example scare you: you only fill in what you use. Most of the fields below are optional, and a brand-new project starts with just a handful.
π± The smallest blueprintβ
This is a complete, valid blueprint:
{
"name": "my-app",
"serverVariation": "frankenphp",
"phpVersion": "8.5",
"database": "sqlite",
"environments": {
"local": {},
"production": {}
}
}
That's enough to run locally and deploy. Everything else on this page is something you add as your needs grow.
π A fuller exampleβ
Here's a more grown-up project β Filament admin, React frontend, a real database, search, caching, file storage, queues, websockets, and three environments (local, staging, production). Use it as a menu, not a checklist:
{
"id": "9f3b7c1e-2d4a-4f9e-8c1d-1b2a3c4d5e6f",
"name": "acme-app",
"blueprints": ["filament"],
"serverVariation": "frankenphp",
"frontend": "react",
"phpVersion": "8.5",
"os": "alpine",
"email": "team@acme.example",
"additionalExtensions": ["gd", "exif"],
"features": [
"octane",
"scheduler",
"horizon",
"scout",
"reverb",
"ssr",
"ai",
"boost",
"mcp"
],
"scoutDriver": "meilisearch",
"packageManager": "npm",
"objectStorage": "minio",
"cacheDriver": "redis",
"database": "postgres",
"strategy": "single-node",
"environments": {
"local": {},
"staging": {
"ingress": "nginx",
"hosts": {
"web": "staging.acme.example",
"reverb": "ws-stg.acme.example",
"s3": "cdn-stg.acme.example"
}
},
"production": {
"managed": ["postgres", "redis"],
"resources": {
"default": {
"requests": { "cpu": "100m", "memory": "256Mi" },
"limits": { "cpu": "1", "memory": "1Gi" }
},
"horizon": {
"limits": { "memory": "2Gi" }
}
},
"hosts": {
"web": "acme.example",
"reverb": "ws.acme.example",
"s3": "cdn.acme.example"
}
}
},
"githubActions": true,
"withCompanions": true,
"provisionTestDb": true,
"lockedFiles": [
"Dockerfile.php",
".env.production",
".env.staging"
],
"watchPaths": [
"app",
"bootstrap",
"config",
"database",
"public",
"resources",
"routes",
"composer.lock",
".env"
]
}
Let's walk through it section by section.
𧬠Project identityβ
"id": "9f3b7c1e-2d4a-4f9e-8c1d-1b2a3c4d5e6f",
"name": "acme-app",
"email": "team@acme.example"
idβ A permanent UUID created the first time you runlarakube init. It acts as the "DNA" of your project. If you move your project folder or rename it, LaraKube CLI still recognizes it by this ID. Your activity history and chat memory in the Console are linked to this ID, ensuring consistency even if your local file path changes. You never edit this by hand.nameβ Your project's short name. LaraKube CLI uses it to label everything it creates in the cluster (each environment gets its own isolated space namedacme-app-local,acme-app-production, and so on). The project folder name must match exactly.emailβ Used when LaraKube CLI requests free HTTPS certificates from Let's Encrypt for your production site.
ποΈ Stack choicesβ
"blueprints": ["filament"],
"serverVariation": "frankenphp",
"frontend": "react",
"phpVersion": "8.5",
"os": "alpine",
"packageManager": "npm",
"additionalExtensions": ["gd", "exif"]
blueprintsβ Ready-made project types (a Filament admin, a Statamic CMS, etc.) that pull in sensible defaults for you. See Multi-Blueprint.serverVariationβ How PHP runs:frankenphp(recommended, great with Octane),fpm-nginx, orfpm-apache. This decides the Docker image LaraKube CLI builds.frontendβreact,vue,svelte, orlivewire. Tells LaraKube CLI whether your app needs a separate Node build step and which Inertia/Echo packages to include.phpVersionβ The PHP version to pin (8.3, 8.4, 8.5).osβalpine(small and fast to start) orbookworm(Debian β broader compatibility if you need certain native extensions).packageManagerβnpm,pnpm,yarn, orbun, for building your frontend assets.additionalExtensionsβ Extra PHP extensions to install. You usually only need one or two here β LaraKube CLI figures out the rest from your features and database choices.
πΎ Data layerβ
"database": "postgres",
"cacheDriver": "redis",
"scoutDriver": "meilisearch",
"objectStorage": "minio"
Your four main data choices: the database, cache, search engine, and file/object storage. Locally, LaraKube CLI runs each one for you (a Postgres, Redis, Meilisearch, and MinIO service), and it fills in the matching DB_*, CACHE_*, SCOUT_*, and AWS_* environment variables automatically β you don't wire those up by hand.
Need a second database (say, MongoDB for analytics alongside Postgres)? Use the plural "databases" array. The same plural form exists for "cacheDrivers", "scoutDrivers", and "objectStorages".
π§© Featuresβ
"features": [
"octane", "scheduler", "horizon",
"scout", "reverb", "ssr",
"ai", "boost", "mcp"
]
The Laravel extras you want turned on. This is one list for the whole project β you don't repeat it per environment.
LaraKube CLI already knows where each feature belongs, and these rules work for any environment name you create (staging, qa, β¦), not just local and production:
| Feature | Turned on in |
|---|---|
horizon, queues, scheduler, reverb, scout, octane | Every environment |
ssr | Every deployed (non-local) environment β locally, Vite already handles SSR for you |
boost, ai, mcp, mailpit | Local only (these are dev tools) |
You almost never need to change this. But if you want to (say, turn on Boost in a throwaway sandbox environment), each environment accepts an addFeatures list (turn something on) and an excludeFeatures list (turn something off).
π Environmentsβ
"environments": {
"local": {},
"staging": { β¦ },
"production": { β¦ }
}
This is the heart of the blueprint. Every project has a local environment; you add staging, production, or any name you like. Each environment's settings are optional β an empty {} (like local above) just means "use the sensible defaults." The settings you can set per environment:
ingressβ Which traffic router sends visitors to your app. Defaults to Traefik (what LaraKube CLI installs for you). You can switch an environment to Nginx with"ingress": "nginx"β handy when an environment lives somewhere that already uses the Nginx ingress controller. (There's an AWS option too β see Advanced: managed Kubernetes.)strategyβsingle-nodeormulti-node-hafor this environment. See Strategy below.managedβ Services you run outside the cluster in this environment. For example,["postgres", "redis"]in production means "I'm using a managed database and Redis from my hosting provider." LaraKube CLI won't deploy its own Postgres/Redis there β it just points your app at the addresses in your.env.production.plexβ Services this environment gets from a shared Plex Commons (one cluster-wide Postgres/Redis/MinIO that several apps share, each with its own isolated database/bucket). A specialisation ofmanaged:larakube plex:joinwrites the connection details into.env.<env>, and LaraKube CLI skips deploying its own copies. See Two Apps, One Server β Plex.hostsβ The real domain names for this environment. See Per-service domains below.cloudβ How LaraKube CLI reaches the cluster for this environment β a VPS (SSH) or a managed kube-context. See Cloud connection below.registryβ The container registry CI/CD builds and pushes to (GHCR / Docker Hub). Required for managed clusters (which can't be SSH-sideloaded). See Container registry below.storageClass,certManagerIssuer, and the managed-cluster knobs (namespace,serviceAccount, β¦) β only needed on managed Kubernetes. See Advanced: managed Kubernetes.resourcesβ Memory and CPU requests/limits for the app pods. Defines adefaultblock plus specific role overrides (e.g. givinghorizonmore memory). LaraKube CLI manages this when you runlarakube resourcesso you don't have to write the JSON by hand.addFeatures/excludeFeaturesβ Turn a feature on or off just for this environment (rarely needed).
Localβ
"local": {}
The simplest case. LaraKube CLI runs everything for you (Postgres, Redis, Meilisearch, MinIO), and every service answers on a friendly *.kube address with HTTPS already trusted β no setup required.
Stagingβ
"staging": {
"ingress": "nginx",
"hosts": {
"web": "staging.acme.example",
"reverb": "ws-stg.acme.example",
"s3": "cdn-stg.acme.example"
}
}
A real server, but still self-contained: LaraKube CLI runs Postgres, Redis, Meilisearch, and MinIO right inside this environment (cheap, isolated, easy to wipe and recreate). Here it uses the Nginx ingress instead of the default Traefik. (Note: The server connection details for this environment are stored separately in .larakube.local.json).
Productionβ
"production": {
"managed": ["postgres", "redis"],
"hosts": {
"web": "acme.example",
"reverb": "ws.acme.example",
"s3": "cdn.acme.example"
}
}
The grown-up environment. It uses the default Traefik ingress (no ingress line needed), and its database and Redis are managed β running on the hosting provider's managed services rather than inside the cluster. LaraKube CLI skips deploying its own Postgres/Redis here and trusts the addresses in your .env.production.
Notice MinIO is not in managed β we're still running our own file storage in production, and the s3 host exposes it at cdn.acme.example.
π Per-service domainsβ
The hosts map is how you give each part of your app its own domain. LaraKube CLI picks a domain using three rules, in order:
- You set it explicitly.
"reverb": "ws.acme.example"puts the Reverb websocket server on its own subdomain. This is the recommended pattern for websockets, file storage, and admin UIs. - It's built from
web. If a service has no domain of its own but you've setweb, LaraKube CLI derives one β e.g. Meilisearch becomesmeilisearch-acme.example. - Local fallback. Locally, everything just answers on
{service}-{name}.kube.
When you run larakube env qa, the wizard automatically asks about every service that can take its own subdomain (Reverb, file storage, Meilisearch, β¦), so you don't have to remember them.
A couple of things worth knowing:
- Horizon and queues don't get their own domain. Horizon shows up at
/horizonon your main site; queues have no UI. - Database/cache dashboards are local-only. LaraKube CLI won't expose a database console to the internet β those only show up locally. For production database access, use an SSH tunnel.
βοΈ Strategy & deploymentβ
"strategy": "single-node",
"githubActions": true,
"withCompanions": true,
"provisionTestDb": true
-
strategyβ How your app is spread across servers:single-nodeβ one server (the classic, low-cost setup). This is the default.multi-node-haβ several servers for high availability. App pods run stateless (per-pod storage, no shared volume), so state is externalized: uploads β S3, sessions/cache β Redis or the database. LaraKube CLI can set this from the cluster's node count when you record the target.
This is the project-wide default, and any environment can override it (e.g. a
single-nodestaging and amulti-node-haproduction from the same blueprint).localis alwayssingle-nodeβ it's just your one machine. -
githubActionsβ Whether the GitHub Actions deploy workflow is set up. Whentrue,larakube gha:configurewires up the secrets and creates the deploy workflow for you. -
withCompanionsβ Whether to include the handy local-only dev apps (Mailpit, phpMyAdmin, RedisInsight, Grafana). Set tofalsefor a leaner local setup. -
provisionTestDbβ Whentrue,larakube test --dbruns your tests against a real copy of your database engine instead of in-memory SQLite. Useful when your tests rely on database-specific features. LaraKube CLI sets this for you the first time you runlarakube test --db.
βοΈ Cloud connection (.larakube.local.json)β
Because .larakube.json is committed to version control, it holds no secrets and no infrastructure coordinates. Server IPs, managed Kubernetes contexts, and SSH keys are machine-specific (and potentially sensitive), so LaraKube CLI stores them in a completely separate, git-ignored file: .larakube.local.json.
You never edit this file by handβyou populate it using larakube cloud:configure:base.
When you do, it will generate an environment block with one of two shapes depending on your cluster:
A VPS (your own server, reached over SSH):
{
"environments": {
"staging": {
"cloud": {
"ip": "203.0.113.20",
"user": "deploy",
"port": 22,
"key": "~/.ssh/id_rsa"
}
}
}
}
ip/user/portβ how to SSH into the server. The kube-context is derived from the IP (larakube-<ip>).keyβ path to your SSH private key (absolute, or~-relative).
A managed cluster (DOKS / EKS / GKE / AKS β no SSH):
{
"environments": {
"production": {
"cloud": {
"context": "do-sgp1-acme",
"provider": "doks"
}
}
}
}
contextβ the kube-context name your provider's CLI wrote into~/.kube/config. LaraKube CLI targets it verbatim; there's noip.providerβdoks,eks,gke,aks, etc. Drives sensible defaults (e.g. the env'sstorageClass).
Either shape also accepts:
archβ target node CPU architecture for the image build:amd64orarm64. Leave it unset and LaraKube CLI auto-detects (over SSH for a VPS, from the cluster's nodes for a managed cluster), falling back toamd64. Set it to force a platform β e.g."arch": "arm64"for a Raspberry Pi or an ARM/Graviton node.
SSH is for you administering the box. To let teammates work with your apps, see Team Access β each person gets their own RBAC-scoped kubeconfig (no SSH, no server login), which works the same on a single VPS or a managed cluster.
π¦ Container registryβ
When LaraKube CLI can't stream the image straight onto the node over SSH β i.e. any managed cluster, and any multi-node setup β it builds the image locally and pushes it to a registry, which the cluster then pulls. Configure that per environment with larakube cloud:configure:registry:
"environments": {
"production": {
"registry": { "provider": "ghcr", "image": "acme/acme-app" }
}
}
providerβghcr(GitHub Container Registry) ordockerhub.imageβ the repository path within the registry (e.g.owner/repo). Optional; defaults sensibly per provider.
The registry choice drives three things in lock-step: the image reference in your manifests, the login/build-push steps in the GitHub Actions workflow, and the image-pull secret created in the cluster. A plain VPS doesn't need this β larakube cloud:deploy sideloads the image over SSH instead.
GHCR images are private in LaraKube CLI β cloud:deploy creates the ghcr-login pull secret automatically from your larakube gha:login token, so there's no "make it public" step. (Docker Hub is the one registry where a public image is an option.)
π Maintenance fieldsβ
"lockedFiles": [
"Dockerfile.php",
".env.production",
".env.staging"
],
"watchPaths": [
"app", "bootstrap", "config", "database",
"public", "resources", "routes",
"composer.lock", ".env"
]
lockedFilesβ Files thatlarakube healwill leave alone. Use this when you've hand-edited a generated file (like a customDockerfile.php) and don't want it overwritten. Add one withlarakube lock <path>, remove withlarakube unlock <path>.watchPathsβ Folderslarakube watchwatches for changes to auto-reload your app during development. The defaults match a standard Laravel project; change them only if your project has an unusual layout.
π€ System fieldsβ
A couple of fields you'll mostly leave alone but might spot when reading other projects' blueprints:
isSystemβtrueonly for LaraKube CLI's own internal projects (like the Console). Alwaysfalsefor your apps.isScaffoldingβ A temporary flag that's onlytruewhilelarakube newis setting a project up. It's stripped out when the file is saved, so you'll never see it in a committed.larakube.json.
π¦ Editing safelyβ
If you hand-edit .larakube.json:
- Check it. Run
larakube heal --forceβ it complains loudly if something's malformed. - Re-generate.
larakube healrewrites everything under.infrastructure/k8s/to match. Your running app keeps running; the nextlarakube upcatches it up. - Commit the blueprint first. The blueprint is the source of truth; the generated files just follow from it.
For most changes β adding a feature, switching the ingress, adding an environment β prefer the dedicated commands (larakube add, larakube env, larakube cloud:configure) over hand-editing. They validate things for you.
π§© Advanced: managed Kubernetesβ
Skip this unless you're deploying to a managed Kubernetes service (DigitalOcean DOKS, AWS EKS, Google GKE, Azure AKS) instead of your own VPS. If you're on a regular server with Traefik or Nginx, you'll never need any of it. For a guided walkthrough, see the DOKS Quickstart.
A managed environment is just one whose cloud block uses context/provider (see Cloud connection) and whose images come from a registry. On top of that, each environment optionally accepts these knobs β every one defaults to the normal VPS behavior, so they only do something when you set them:
storageClassβ the StorageClass for this env's volumes. Usually set for you fromprovider(DOKS βdo-block-storage, EKS βgp3, GKE βstandard, AKS βmanaged-premium). Unset = the cluster default.certManagerIssuerβ turn on automatic HTTPS via cert-manager: the name of aClusterIssuer(e.g."letsencrypt-prod"). LaraKube CLI adds the issuer annotation to the ingress. Requires cert-manager already installed on the cluster. (On a LaraKube CLI-provisioned DOKS cluster, Traefik + Let's Encrypt is installed for you, so you useingressAnnotationsinstead β see below.)namespaceβ deploy into an existing cluster space instead of LaraKube CLI's default{name}-{environment}.serviceAccount(andserviceAccountAnnotations) β run your app under a specific cluster identity, e.g. to give it cloud permissions (EKS IRSA, GKE Workload Identity).imagePullSecret/omitImagePullSecretβ change or drop the credential used to pull your container image (some clusters pull images using the server's own cloud role, so no secret is needed).ingressAnnotationsβ extra settings passed straight through to your ingress/load balancer (for example, a TLS certificate ID, a firewall group, or β on a DOKS Traefik cluster βtraefik.ingress.kubernetes.io/router.tls.certresolver: letsencryptto fetch a cert for this app).
A managed production therefore looks like this in your main blueprint:
"production": {
"registry": { "provider": "ghcr", "image": "acme/acme-app" },
"storageClass": "do-block-storage",
"hosts": { "web": "acme.example", "s3": "cdn.acme.example" },
"plex": ["postgres", "redis", "minio"]
}
(Remember: the "cloud": { "context": "do-sgp1-acme" } block for this environment is securely generated in .larakube.local.json when you run larakube cloud:configure:base).
If any of the terms above are unfamiliar, that's a sign you don't need this section yet.
On a managed cluster every host β your app, plus any shared S3/Commons host β resolves to the same ingress LoadBalancer IP. Point one DNS A record ("anchor") at that IP, then CNAME every host to the anchor. Each new app you add is just another CNAME, and if the IP ever changes you update a single record. larakube cloud:provision:doks, plex:init, and cloud:deploy all print this guidance with your real IP.
π§ Console Memory & Syncβ
LaraKube treats your .larakube.json as infrastructure as code, but it also pairs this with a global SQLite database maintained by the LaraKube Console. This provides a unified history for all your projects without scattering logs across different folders.
Whenever you run a command via the CLI, it automatically "calls home" to the Console API to register the event. This real-time sync tracks:
- Operational Verbs: Every
up,down,heal, orpurgecommand. - Architectural Evolution: When you add features or swap databases via the CLI or UI.
- AI Diagnostics: Full reports from the
doctorcommand and AI-powered fix history.
This ensures your visual dashboard and AI agents always have an accurate, up-to-date memory of your project's evolution, even if you prefer working entirely in the terminal.