Deploying
This guide covers packaging and deploying a Crate API to production. It assumes you have a working project with environment variable substitution set up in your config files — see the Configuration section in the Build a Complete API tutorial.
Building for Production
Section titled “Building for Production”Crate projects compile to a single native binary. Build a release version:
dub build -b releaseThe output binary has no runtime dependencies beyond the system’s C library and the D shared runtime. Copy it to your server along with the config/ directory and you’re done.
Docker
Section titled “Docker”Create a deploy/Dockerfile. The binary is compiled on the host (or in CI) and copied into the image:
FROM fedora:42
LABEL maintainer="you@example.com"
ARG APP_NAME=blog-api
COPY service/$APP_NAME /app/app-bin
COPY config/configuration.docker.json /app/config/configuration.jsonCOPY config/db.model/mongo.json /app/config/db/mongo.json
RUN mkdir -p /app/config/db && \ mkdir -p /app/files && \ mkdir -p /app/logs && \ dnf install -y openssl ImageMagick librsvg2-tools
# ServiceENV serviceName blog-api # Display name used in logs and statsENV serviceUrl http://localhost # Public URL of the service (used for links, redirects)ENV apiUrl http://localhost # Public base URL for API endpointsENV hostName localhost # Hostname the HTTP server binds toENV logLevel 4 # vibe.d log verbosity (0=all, 4=warn, 6=critical)
# MongoENV mongoUrl "" # Full MongoDB connection string (mongodb://user:pass@host:port/db)ENV mongoHost localhost # MongoDB hostname, used by db-service DNS in KubernetesENV mongoDBName blog # MongoDB database name, drives data isolation per tenant
# Data volumesVOLUME ["/app/config", "/app/files", "/app/logs"]WORKDIR /app
EXPOSE 80
ENTRYPOINT ["/app/app-bin"]The image installs ImageMagick and librsvg for image resizing (used by CrateImageCollectionResource). If your API doesn’t handle image uploads, you can drop those packages.
Three volumes are declared: config for runtime configuration overrides, files for filesystem-based file storage, and logs for application logs.
Build and run:
mkdir -p deploy/servicecp blog-api deploy/service/cd deploydocker build -t blog-api --build-arg APP_NAME=blog-api .docker run -e mongoUrl=mongodb://mongo:27017/blog -p 80:80 blog-apiOverride env vars per environment:
docker run -e mongoUrl=mongodb://prod-db:27017/blog \ -e apiUrl=https://api.example.com \ -e mongoDBName=blog-prod \ -p 80:80 blog-apiDocker Compose
Section titled “Docker Compose”For local development with MongoDB:
services: api: build: context: ./deploy args: APP_NAME: blog-api ports: - "80:80" environment: mongoUrl: mongodb://mongo:27017/blog mongoDBName: blog mongoHost: mongo apiUrl: http://localhost depends_on: - mongo
mongo: image: mongo:7 ports: - "27017:27017" volumes: - mongo-data:/data/db
volumes: mongo-data:Start everything:
docker compose upMongoDB data persists in the mongo-data volume across restarts. The API connects using the mongo service name as the hostname — Docker’s internal DNS resolves it automatically.
If you use GridFS for file storage, no extra volume is needed — files are stored in MongoDB alongside your data.
Helm / Kubernetes
Section titled “Helm / Kubernetes”Chart Structure
Section titled “Chart Structure”helm/blog-api/ Chart.yaml values.yaml templates/ _helpers.tpl api.yaml ingress.yaml certificate.yamlChart.yaml
Section titled “Chart.yaml”apiVersion: v1name: blog-apiversion: 0.1.0_helpers.tpl
Section titled “_helpers.tpl”Define name helpers so service discovery works through Helm-computed names:
{{- define "blog-api.fullname" -}}{{- printf "%s-%s" .Release.Name "api" | trunc 63 | trimSuffix "-" -}}{{- end -}}values.yaml
Section titled “values.yaml”Keep it flat. Non-sensitive values go here, secrets are passed at install time via --set:
domainName: "api.example.com"serviceName: "blog-api"mongoDBName: "blog"mongoUrl: ""
logLevel: 4
api: image: repository: registry.example.com/blog-api tag: latest pullPolicy: IfNotPresent replicas: 2 environmentVars: - envName: "mongoHost" envValue: "db-service"templates/api.yaml
Section titled “templates/api.yaml”A Deployment and Service in the same file — the standard Crate deployment unit:
apiVersion: apps/v1kind: Deploymentmetadata: name: {{ include "blog-api.fullname" . }} labels: app: {{ include "blog-api.fullname" . }}spec: replicas: {{ .Values.api.replicas }} selector: matchLabels: app: {{ include "blog-api.fullname" . }} template: metadata: labels: app: {{ include "blog-api.fullname" . }} spec: containers: - name: api image: {{ .Values.api.image.repository }}:{{ .Values.api.image.tag }} imagePullPolicy: {{ .Values.api.image.pullPolicy }} ports: - containerPort: 80 env: {{- range .Values.api.environmentVars }} - name: {{ .envName }} value: "{{ .envValue }}" {{- end }} - name: serviceName value: {{ .Values.serviceName }} - name: apiUrl value: https://{{ .Values.domainName }} - name: hostName value: {{ .Values.domainName }} - name: mongoDBName value: {{ .Values.mongoDBName }} - name: mongoUrl value: {{ .Values.mongoUrl }} - name: logLevel value: "{{ .Values.logLevel }}"---apiVersion: v1kind: Servicemetadata: name: {{ include "blog-api.fullname" . }}-servicespec: selector: app: {{ include "blog-api.fullname" . }} ports: - protocol: TCP port: 80 targetPort: 80 name: httpEnvironment variables are split into two groups: the environmentVars list for per-deployment overrides (like mongoHost), and the static block derived from top-level values. This keeps values.yaml clean while allowing flexibility.
templates/ingress.yaml
Section titled “templates/ingress.yaml”Route external traffic to the API through nginx ingress:
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: {{ include "blog-api.fullname" . }}-ingress annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/rewrite-target: /$1 nginx.ingress.kubernetes.io/proxy-body-size: 20mspec: tls: - hosts: - {{ .Values.domainName }} secretName: {{ include "blog-api.fullname" . }}-cert rules: - host: {{ .Values.domainName }} http: paths: - path: /(.*) pathType: ImplementationSpecific backend: service: name: {{ include "blog-api.fullname" . }}-service port: number: 80The proxy-body-size annotation is important if your API handles file uploads — nginx’s default limit is 1MB.
templates/certificate.yaml
Section titled “templates/certificate.yaml”Automate TLS with cert-manager:
apiVersion: cert-manager.io/v1kind: Certificatemetadata: name: {{ include "blog-api.fullname" . }}-certspec: secretName: {{ include "blog-api.fullname" . }}-cert issuerRef: name: letsencrypt-prod kind: ClusterIssuer dnsNames: - {{ .Values.domainName }}This requires a ClusterIssuer named letsencrypt-prod to be configured in your cluster.
External Database
Section titled “External Database”If MongoDB runs outside the cluster (a dedicated VM, managed service, etc.), create a headless Service with static Endpoints so pods can reach it by DNS name:
# db-service.yaml (apply once, not part of the Helm chart)kind: ServiceapiVersion: v1metadata: name: db-servicespec: clusterIP: None ports: - port: 27017---kind: EndpointsapiVersion: v1metadata: name: db-servicesubsets: - addresses: - ip: 10.0.1.50 ports: - port: 27017 name: db-serviceThen set mongoHost: db-service in your environmentVars and include the same hostname in mongoUrl. Pods resolve db-service through cluster DNS to your external IP.
Deploy
Section titled “Deploy”helm install --namespace blog blog ./helm/blog-api \ --set mongoDBName=blog \ --set domainName="api.example.com" \ --set mongoUrl="mongodb://user:pass@db-service:27017/blog"The three values you always override at install time are mongoDBName, domainName, and mongoUrl. Everything else uses chart defaults.
Per-Environment Overrides
Section titled “Per-Environment Overrides”Create a values file per environment:
api: replicas: 1domainName: "staging-api.example.com"logLevel: 6helm install blog ./helm/blog-api -f values.staging.yaml \ --set mongoUrl="mongodb://user:pass@db-service:27017/blog-staging"Multi-Tenancy
Section titled “Multi-Tenancy”The same chart can deploy multiple independent instances by varying mongoDBName and domainName. Each tenant gets its own database and hostname:
helm install tenant-a ./helm/blog-api \ --set mongoDBName=tenant-a \ --set domainName="a.example.com" \ --set mongoUrl="mongodb://db-service:27017/tenant-a"
helm install tenant-b ./helm/blog-api \ --set mongoDBName=tenant-b \ --set domainName="b.example.com" \ --set mongoUrl="mongodb://db-service:27017/tenant-b"Base URL Path Prefix
Section titled “Base URL Path Prefix”The HTTPConfig struct supports an optional baseUrl field that prepends a path prefix to all routes. This is useful when running behind a reverse proxy that strips or rewrites path prefixes:
{ "http": { "hostName": "0.0.0.0", "baseUrl": "/api/v1" }}With this configuration, all endpoints are prefixed:
| Without baseUrl | With baseUrl /api/v1 |
|---|---|
POST /mcp | POST /api/v1/mcp |
GET /users | GET /api/v1/users |
GET /.well-known/oauth-protected-resource | GET /.well-known/oauth-protected-resource (unchanged) |
The baseUrl field is optional and defaults to an empty string (no prefix). Well-known endpoints are not affected by the prefix — they remain at their standard paths.
This combines with per-policy PolicyConfig base URLs. For example, if HTTPConfig.baseUrl is /api and you use McpPolicy!(PolicyConfig("/v1")), the MCP endpoint will be at /api/v1/mcp.
Configuration Flow Summary
Section titled “Configuration Flow Summary”The same $ and # substitution syntax in your config JSON files works identically across all deployment methods. The only thing that changes is how the environment variables are injected:
| Method | How env vars are set |
|---|---|
| Local dev | Shell exports or .env file |
| Docker | ENV in Dockerfile, -e flag on docker run |
| Compose | environment: block in docker-compose.yml |
| Kubernetes | env: in pod spec, from values and --set overrides |
| systemd | Environment= in service unit file |
The config JSON files stay the same across all environments. Only the values change.