197 lines
8.4 KiB
Markdown
197 lines
8.4 KiB
Markdown
# 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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```bash
|
|
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.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:
|
|
|
|
```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:
|
|
|
|
```yaml
|
|
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`:
|
|
|
|
```yaml
|
|
ports:
|
|
- app.example.com:3000
|
|
```
|
|
|
|
kuber also supports:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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
|
|
|
|
```bash
|
|
# 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.
|