Docker Deployment
Self-hosting Dyrected in a container.
Dyrected has no official pre-built Docker image. For self-hosted deployments you build your own image from your Next.js or Nuxt application — the Dyrected engine runs inside your app, not as a separate service.
Dockerfile (Next.js)
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]This uses Next.js standalone output. Add
output: 'standalone'to yournext.config.ts.
Docker Compose
A complete self-hosted setup with PostgreSQL and Redis:
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- '3000:3000'
environment:
NODE_ENV: production
DATABASE_URL: postgresql://dyrected:password@db:5432/dyrected
REDIS_URL: redis://redis:6379
DYRECTED_JWT_SECRET: ${DYRECTED_JWT_SECRET}
DYRECTED_API_KEY: ${DYRECTED_API_KEY}
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
volumes:
- uploads:/app/public/uploads # persist local file uploads
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: dyrected
POSTGRES_USER: dyrected
POSTGRES_PASSWORD: password
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dyrected"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redisdata:/data
restart: unless-stopped
volumes:
pgdata:
redisdata:
uploads:Store secrets in a .env file alongside docker-compose.yml:
# .env (never commit this)
DYRECTED_JWT_SECRET=a-long-random-secret
DYRECTED_API_KEY=sk_live_...Health check
Add a health check endpoint to your app and reference it in your orchestrator:
// app/api/health/route.ts (Next.js)
export async function GET() {
return Response.json({ ok: true })
}Docker Compose health check:
app:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10sEnvironment variables
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | ✅ | PostgreSQL connection string |
DYRECTED_JWT_SECRET | ✅ | Secret used to sign JWTs. Use a long random string. |
DYRECTED_API_KEY | ✅ | Site API key for server-to-server requests |
REDIS_URL | Cloud mode | Redis connection string (required for Cloud live preview token mode) |
NODE_ENV | Set to production to disable Ethereal email fallback and dev warnings |
Persistent volumes
| Path | What it stores | Notes |
|---|---|---|
/app/public/uploads | Files from LocalStorageAdapter adapter | Not needed if using S3/Cloudinary |
/var/lib/postgresql/data | PostgreSQL data | Mount in the db service |
/data (Redis) | Redis AOF/RDB persistence | Mount in the redis service |
If you use
LocalStorageAdapterfor file uploads, theuploadsvolume must be mounted. For production, prefer an S3-compatible storage adapter — it works across multiple replicas and doesn't need a volume.