feat: add examples

This commit is contained in:
2026-08-30 00:23:53 +07:00 Unverified
parent d259b49b8a
commit 32ec172335
6 changed files with 354 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
# Dependencies and generated output
node_modules
.next
out
dist
build
coverage
*.tsbuildinfo
# Source-control and local tooling
.git
.gitignore
.github
.idea
.vscode
.DS_Store
# Logs and test artifacts
*.log
playwright-report
test-results
# Runtime secrets are read by kuber from compose.yml, not copied into the image.
.env
.env.*
!.env.example
# Deployment files are not needed by the application runtime.
Dockerfile*
compose*.yml
compose*.yaml
docker-compose*.yml
docker-compose*.yaml
+33
View File
@@ -0,0 +1,33 @@
# syntax=docker/dockerfile:1.7
FROM oven/bun:canary-alpine AS base
RUN addgroup -g 1001 app && adduser -D -u 1001 -G app app
WORKDIR /app
FROM base AS dependencies
USER app
COPY --chown=1001:1001 package.json bun.lock ./
RUN --mount=type=cache,target=/home/app/.bun,uid=1001,gid=1001 \
bun install --frozen-lockfile
FROM dependencies AS build
COPY --chown=1001:1001 . .
# Keep the public directory available even when the application does not use one.
RUN mkdir -p public && bun run build
FROM base AS runner
ENV NODE_ENV=production \
HOSTNAME=0.0.0.0 \
PORT=3000
USER app
COPY --from=build --chown=1001:1001 /app/.next/standalone ./
COPY --from=build --chown=1001:1001 /app/.next/static ./.next/static
COPY --from=build --chown=1001:1001 /app/public ./public
EXPOSE 3000
CMD ["bun", "server.js"]
+190
View File
@@ -0,0 +1,190 @@
# 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. 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:
```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`.
`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.
+88
View File
@@ -0,0 +1,88 @@
# kuber derives the Kubernetes namespace and built image names from the current
# directory. Run kuber from the repository root that contains this file.
services:
app:
build:
context: .
dockerfile: Dockerfile
target: runner
# NEXT_PUBLIC_* values are embedded by `next build`, so pass them as
# build args only when your Dockerfile declares matching ARG/ENV entries.
# args:
# NEXT_PUBLIC_API_URL: https://api.example.com
# Use `image` instead of `build` when an external registry already owns the
# image lifecycle. A service should normally use one approach or the other.
# image: registry.example.com/team/app:latest
# If the app has a .env file, uncomment this. kuber reads it locally and
# creates an `app-env` Kubernetes Secret; Docker never copies it.
# env_file:
# - .env
environment:
NODE_ENV: production
HOSTNAME: 0.0.0.0
PORT: "3000"
# Application data services are declared with kuber pseudo-volumes. They
# inject credentials into app-env but do not mount a filesystem. Uncomment
# `volumes` and only the services this application needs.
# volumes:
# # Many applications need Postgres. This injects DATABASE_URL.
# - postgresql:app
# # Some applications also need object storage. This provisions an S3
# # key named app-assets and bucket named assets, then injects AWS/S3 env.
# - s3:app-assets/assets
#
# # Filesystem persistence is uncommon for Next.js. Prefer Postgres or S3
# # unless the application specifically requires POSIX file access.
# # A file bind becomes a ConfigMap.
# - ./config/app.json:/app/config/app.json:ro
# # A directory bind or named volume becomes a PVC.
# - ./uploads:/app/uploads
# - cache(5Gi on 1 fast):/app/.next/cache
ports:
# A hostname creates an Ingress that routes to container port 3000.
- app.example.com:3000
# Other supported routing forms:
# - "*.example.com:3000"
# - admin.example.com:3000:protected
# - admin.example.com:3000:protected(/admin,/api/internal)
deploy:
replicas: 1
# !! You probably will not need this section in most deployments. !!
# These Kubernetes container fields are merged into the generated
# Deployment. TCP probes work without requiring a dedicated health route.
x-container:
readinessProbe:
tcpSocket:
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
runAsGroup: 1001
runAsNonRoot: true
runAsUser: 1001
seccompProfile:
type: RuntimeDefault
+6
View File
@@ -0,0 +1,6 @@
const nextConfig = {
// The Dockerfile copies this minimal server and its traced dependencies.
output: "standalone",
} as const;
export default nextConfig;