Files
kuber/example
T
..
2026-08-30 00:23:53 +07:00
2026-08-30 00:23:53 +07:00
2026-08-30 00:23:53 +07:00
2026-08-30 00:23:53 +07:00
2026-08-30 00:23:53 +07:00

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. kuber uses the configured SSH builder for image builds and the Kubernetes client with ~/.kube/config for cluster operations.

Image Build

The Dockerfile has four responsibilities:

  • install dependencies from package.json and bun.lock with a BuildKit cache
  • run the application's normal bun run build script
  • 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.

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:ro turns a local file into a ConfigMap
  • ./uploads:/app/uploads turns a directory bind into a PVC-backed mount
  • cache(5Gi on 1 fast):/app/.next/cache creates 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.