feat: add examples
This commit is contained in:
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
const nextConfig = {
|
||||
// The Dockerfile copies this minimal server and its traced dependencies.
|
||||
output: "standalone",
|
||||
} as const;
|
||||
|
||||
export default nextConfig;
|
||||
Reference in New Issue
Block a user