8.4 KiB
Next.js with kuber
This directory is a deployment template for an existing Bun-powered Next.js application. It deliberately does not contain a package.json, application source, or an initialized Next.js project.
Place Dockerfile, .dockerignore, and compose.yml at the root of an existing Next.js repository. Merge the output: "standalone" setting from next.config.ts into that application's Next configuration.
The expected project shape is:
my-next-app/
├── app/ or pages/
├── public/ # optional; the Dockerfile creates it when absent
├── package.json
├── bun.lock
├── next.config.ts
├── Dockerfile
├── .dockerignore
└── compose.yml
Standard Deployment
Update app.example.com in compose.yml, then run these commands from the Next.js repository root:
kuber export -o manifests.yaml
kuber up
kuber ps
kuber logs -f app
export renders the Kubernetes resources without applying them. up builds and pushes the image when necessary, applies the resources, restarts deployments whose image content changed, and waits for rollout.
The current directory name determines the Kubernetes namespace and forms part of built image names. The Compose name field does not override that kuber behavior.
Docker and kubectl are not required on the workstation. Image builds happen
inside the cluster: kuber uploads the repository snapshot to the management
service, which runs a rootless BuildKit Job that builds and pushes the image.
Cluster operations go through the authenticated v2 API, not a local
~/.kube/config.
Image Build
The Dockerfile has four responsibilities:
- install dependencies from
package.jsonandbun.lockwith a BuildKit cache - run the application's normal
bun run buildscript - consume Next.js's
output: "standalone"server bundle - run that bundle as an unprivileged user on
0.0.0.0:3000
The runner stage contains only the standalone server, traced runtime dependencies, static assets, and public files. The Compose service explicitly selects this stage.
Before starting a remote build, kuber reconstructs the local repository on the builder from:
- the committed Git state
- tracked local changes
- untracked files
- ignored
.env*files
Docker then applies .dockerignore to the build context. In particular, .env* files are transferred so kuber can read them, but they are excluded from the image build and are not baked into an image layer.
When the resulting image has the same content fingerprint as the registry image, kuber up does not restart that deployment unnecessarily.
Environment Variables
The example documents two different environment mechanisms.
Values under environment are written directly into the generated container specification. These are suitable for non-secret runtime configuration such as NODE_ENV, HOSTNAME, and PORT.
If the application has a .env file, uncomment env_file in compose.yml. kuber reads it on the workstation, creates an opaque Kubernetes Secret named app-env, and attaches it to the Deployment with envFrom. The file is not copied into the container image. Keep the entry commented when no .env file exists.
Explicit environment values take precedence over duplicate names loaded through env_file by Kubernetes. Avoid putting NODE_ENV, HOSTNAME, or PORT in both places unless that override is intentional.
Next.js NEXT_PUBLIC_* values are different: Next embeds them into browser bundles during next build. If the application needs one, declare it before the build command in the Dockerfile:
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN mkdir -p public && bun run build
Then enable the matching Compose build argument:
build:
context: .
dockerfile: Dockerfile
target: runner
args:
NEXT_PUBLIC_API_URL: https://api.example.com
Do not pass secrets through build arguments or NEXT_PUBLIC_*; use env_file for runtime secrets.
Routing
This port entry creates a Kubernetes Service and an Ingress routing the hostname to container port 3000:
ports:
- app.example.com:3000
kuber also supports:
ports:
- "*.example.com:3000"
- admin.example.com:3000:protected
- admin.example.com:3000:protected(/admin,/api/internal)
Wildcard entries must be quoted so YAML does not interpret * as an alias. protected routes use the cluster's routing/cf-auth middleware. A path list protects only the listed prefixes; omitting it protects the whole host.
The active example uses TCP readiness and liveness probes, so it does not assume the application exposes a particular health-check route. Replace those probes with HTTP probes if the application has a stable endpoint designed for health checks.
Replicas and Resources
deploy.replicas controls the Deployment replica count. The example starts with one replica and includes conservative CPU and memory requests and limits through x-container.
A "min-max" range enables autoscaling: kuber renders a HorizontalPodAutoscaler (CPU 80% target) so the Deployment scales between the min and max, and injects a 100m CPU request if none is set. For example deploy.replicas: "1-6".
x-container is merged directly into the generated Kubernetes container. The example uses it for probes, resources, and a non-root security context matching UID/GID 1001 from the Dockerfile.
Tune resource values from observed production usage. Memory limits that are too low cause Kubernetes to terminate the process, while requests that are too high make scheduling unnecessarily difficult.
Data Services
Many applications need Postgres even though the Next.js container itself remains stateless. Uncomment the volumes block and its Postgres entry:
volumes:
- postgresql:app
This is a kuber pseudo-volume: it provisions managed Postgres and injects DATABASE_URL into the generated app-env Secret. It does not create a filesystem mount. An explicit database username/name can be requested with postgresql:user/database.
Some applications also store uploads, generated media, exports, or other objects in S3:
volumes:
- s3:app-assets/assets
This provisions an S3 access key named app-assets and a bucket named assets. kuber injects AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ENDPOINT_URL_S3, AWS_REGION, and S3_BUCKET into app-env.
Postgres and S3 can be declared together under the same volumes key. Their generated values are merged with values read from env_file.
Normal kuber down retains managed databases, managed S3 resources, ingress, PVCs, and the namespace. kuber down -f removes those retained resources as well. Use the force form only when permanent data deletion is intended.
Filesystem Storage
Filesystem persistence is comparatively rare for Next.js. Prefer Postgres for structured data and S3 for files unless the application specifically requires POSIX filesystem semantics.
The remaining commented examples in compose.yml demonstrate kuber's filesystem forms:
./config/app.json:/app/config/app.json:roturns a local file into a ConfigMap./uploads:/app/uploadsturns a directory bind into a PVC-backed mountcache(5Gi on 1 fast):/app/.next/cachecreates a named PVC with explicit size and placement hints
Operations
# Build, apply, and wait for rollout
kuber up
# Apply and start without rebuilding images
kuber start
# Inspect deployments and logs
kuber ps
kuber logs app
kuber logs -f app
# Run a command in the application container
kuber exec app sh
# Restart or temporarily scale deployments down
kuber restart
kuber stop
# Render manifests for inspection
kuber export -o manifests.yaml
# Delete stateless resources but retain persistent infrastructure
kuber down
# Also delete retained ingress, storage, databases, S3, and namespace
kuber down -f
Application Boundary
Most Next.js projects need only the single app service shown here. Route handlers, server actions, and other code executed by the Next.js server remain part of that service.
Some applications, including frontend/backend systems such as Buzz or Yaup, also produce an independently running API, worker, scheduler, or collector. Those processes need their own Docker build target, command, Compose service, Deployment, resource limits, and lifecycle. They are application-specific and are intentionally not included in this standard Next.js template.