Skip to content

Docker — Embedded Mode

In embedded mode, the gateway serves static files directly — no nginx, no Caddy, no Node.js. This produces the smallest possible production image.

# Stage 1: Build the frontend
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: FROM scratch — nothing but the binary and static files
FROM scratch
COPY --from=build /app/dist /static
COPY --from=ghcr.io/ruachtech/rep/gateway:latest \
/usr/local/bin/rep-gateway /rep-gateway
EXPOSE 8080
USER 65534:65534
ENTRYPOINT ["/rep-gateway"]
CMD ["--mode", "embedded", "--static-dir", "/static", "--port", "8080", "--strict"]

Exactly two things:

  • Your built static files (dist/)
  • The REP gateway binary (~3MB)

No OS, no shell, no utilities, no package manager. Attack surface: near zero.

Terminal window
docker build -t myapp .
docker run -p 8080:8080 \
-e REP_PUBLIC_API_URL=https://api.example.com \
-e REP_SENSITIVE_ANALYTICS_KEY=UA-XXXXX \
-e REP_GATEWAY_STRICT=true \
-e REP_GATEWAY_ALLOWED_ORIGINS=https://app.example.com \
myapp
# In the gateway download stage
ARG TARGETARCH
# The gateway image is multi-arch — Docker selects the correct binary
COPY --from=ghcr.io/ruachtech/rep/gateway:latest \
/usr/local/bin/rep-gateway /rep-gateway

The FROM scratch + non-root pattern provides:

  • No shelldocker exec attacks are impossible
  • No package manager — no malware installation vector
  • Read-only filesystem — compatible with readOnlyRootFilesystem: true
  • Non-root user — UID 65534 (nobody)
  • Static binary — no shared libraries, no dynamic linker
  • You want the smallest possible container image
  • You’re deploying a pure SPA with no server-side rendering
  • Security is a priority (minimal attack surface)
  • You don’t need nginx-specific features (URL rewriting, basic auth, etc.)