Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Rust build artifacts β€” never send to Docker daemon
# Rust build artifacts
target/
**/*.rs.bk

Expand Down
63 changes: 27 additions & 36 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
# =============================================================================
# minikv - Multi-Stage Dockerfile
# minikv multi-stage Dockerfile
#
# Stages:
# 1. chef - installs cargo-chef for dependency caching
# 2. planner - computes the dependency recipe
# 3. builder - compiles dependencies (cached), then the binary
# 4. runtime - minimal distroless image with only the binary
# Build stages:
# 1. chef - installs cargo-chef for dependency layer caching
# 2. planner - computes the dependency recipe
# 3. builder - compiles dependencies (cached) and the binary
# 4. runtime - minimal distroless image containing only the binary
#
# BLAKE3 hashing requires no external C libs - fully pure Rust.
# rusty-leveldb is pure Rust - no libleveldb.so dependency.
# Final image has zero shell, zero package manager, zero attack surface.
# =============================================================================
# BLAKE3 and rusty-leveldb are pure Rust implementations.
# No external C libraries are required at runtime.
# The final image contains no shell and no package manager.

# -----------------------------------------------------------------------------
# Stage 1: chef
# Installs cargo-chef for layer-cached dependency compilation.
# Installs cargo-chef for reproducible dependency caching.
# -----------------------------------------------------------------------------
FROM rust:1.88-slim-bookworm AS chef

# Install cargo-chef for reproducible dependency caching
RUN cargo install cargo-chef --locked

WORKDIR /build

# -----------------------------------------------------------------------------
# Stage 2: planner
# Computes the dependency recipe from Cargo.toml + Cargo.lock.
# This layer only re-runs when dependencies change.
# Generates the dependency recipe from Cargo manifests.
# This layer changes only when dependency definitions change.
# -----------------------------------------------------------------------------
FROM chef AS planner

Expand All @@ -38,68 +35,62 @@ RUN cargo chef prepare --recipe-path recipe.json

# -----------------------------------------------------------------------------
# Stage 3: builder
# Compiles dependencies first (cached layer), then the application.
# Compiles dependencies first (cached), then the application.
# -----------------------------------------------------------------------------
FROM chef AS builder

# Build-time dependencies only - no runtime C libs needed.
# rusty-leveldb and blake3 are both pure Rust.
# Build-time dependencies only.
RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config \
&& rm -rf /var/lib/apt/lists/*

COPY --from=planner /build/recipe.json recipe.json

# Compile dependencies - this layer is cached unless Cargo.toml/lock changes
RUN cargo chef cook --release --recipe-path recipe.json

# Copy full source and compile the application binary
COPY Cargo.toml Cargo.lock ./
COPY minikv ./minikv
COPY minikv-core ./minikv-core
COPY config ./config

# Build release binary
# RUSTFLAGS for correctness: deny unused, warn on unsafe
# Enforce strict compilation rules.
ENV RUSTFLAGS="-D warnings -D unsafe_code"

# Build release binary and strip symbols.
RUN cargo build --release --locked \
&& strip target/release/minikv

# -----------------------------------------------------------------------------
# Stage 4: Distroless image runtime
# Stage 4: runtime
# Distroless base image containing only required runtime components.
# -----------------------------------------------------------------------------
FROM gcr.io/distroless/cc-debian12:nonroot AS runtime

# Metadata
LABEL org.opencontainers.image.title="minikv"
LABEL org.opencontainers.image.description="Tiny distributed key value store in pure Rust"
LABEL org.opencontainers.image.description="Distributed key value store in Rust"
LABEL org.opencontainers.image.source="https://github.com/ekkolon/minikv"
LABEL org.opencontainers.image.licenses="MIT"

# Copy the stripped binary from builder
# Copy compiled binary.
COPY --from=builder /build/target/release/minikv /usr/local/bin/minikv

# Copy nginx config (used by operators, not the binary itself)
# Copy nginx reference configuration (for operators).
COPY --from=builder /build/config/nginx-volume.conf /etc/minikv/nginx-volume.conf

# Data directory for LevelDB - must be mounted as a volume in production
# The nonroot user (uid=65532) must own this path
# Working directory for the metadata database.
# This path must be mounted as a volume in production.
WORKDIR /data

# Expose the default server port
# Override with: minikv server --port <port>
# Default server port. Can be overridden via CLI flag.
EXPOSE 3000

# Run as nonroot (distroless nonroot image sets this by default)
# UID 65532 - no shell, no sudo, no privilege escalation possible
# Run as non-root user (UID 65532).
USER nonroot

# Default entrypoint - subcommand must be passed at runtime:
# Entry point. Subcommand must be provided at runtime, for example:
# docker run minikv server --port 3000 --db /data --volumes ...
# docker run minikv rebuild ...
# docker run minikv rebalance ...
ENTRYPOINT ["/usr/local/bin/minikv"]

# No default CMD - operator must provide subcommand explicitly.
# This prevents accidental runs with wrong configuration.
# No default CMD. A subcommand must be specified explicitly.
97 changes: 44 additions & 53 deletions config/nginx-frontend.conf
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
# =============================================================================
# X-Accel-Redirect reverse proxy for minikv
#
# DNS RESOLUTION NOTE:
# nginx resolves upstream hostnames at *startup* by default. If the upstream
# (coordinator, volume servers) is not yet in DNS, nginx refuses to start.
# DNS resolution
#
# To overcome this, we use `resolver` + a variable for every upstream.
# When the upstream is stored in a variable, nginx defers DNS resolution
# to *request time*, so startup succeeds even if backends aren't running yet.
# By default, nginx resolves upstream hostnames at startup. If an upstream
# service (coordinator or volume server) is not yet resolvable, nginx fails
# to start.
#
# Docker's internal DNS resolver is always at 127.0.0.11.
# =============================================================================
# To avoid this, upstreams are stored in variables and a `resolver` is
# configured. When `proxy_pass` references a variable, DNS resolution
# happens at request time instead of startup time.
#
# In Docker environments, the internal DNS resolver is available at
# 127.0.0.11.

worker_processes auto;
error_log /dev/stderr warn;
Expand All @@ -28,34 +29,30 @@ http {
server_tokens off;
default_type application/octet-stream;

# Docker's internal DNS β€” required for runtime upstream resolution.
# `valid=5s` re-resolves every 5 seconds so container restarts are
# picked up quickly without reloading nginx.
# Docker internal DNS for runtime upstream resolution.
# `valid=5s` forces periodic re-resolution so container restarts
# are detected without reloading nginx.
resolver 127.0.0.11 valid=5s ipv6=off;

server {
listen 8080 default_server;
server_name _;

# ------------------------------------------------------------------
# Coordinator upstream as a variable β€” defers DNS to request time.
# Service name matches docker-compose: "minikv"
# ------------------------------------------------------------------
# Coordinator upstream stored in a variable to defer DNS resolution
# to request time. Service name matches docker-compose ("minikv").
set $coordinator_upstream "minikv:3000";

# ------------------------------------------------------------------
# Main proxy: all client requests go to the coordinator.
# Main proxy. All client requests are forwarded to the coordinator.
#
# On GET/HEAD the coordinator returns:
# X-Accel-Redirect: /accel/volume1:8080/sv09/a2/38/...
# Content-Type: image/jpeg
# Content-Blake3: <hash>
# Key-Balance: balanced
# For GET and HEAD, the coordinator responds with:
# X-Accel-Redirect: /accel/<volume>/<object-path>
# Content-Type
# Content-Blake3
# Key-Balance
#
# nginx intercepts X-Accel-Redirect and performs an internal
# subrequest, streaming the object body to the client with the
# coordinator's headers intact.
# ------------------------------------------------------------------
# nginx intercepts X-Accel-Redirect and performs an internal subrequest.
# The object body is streamed from the volume server while preserving
# coordinator-provided metadata headers.
location / {
proxy_pass http://$coordinator_upstream;

Expand All @@ -64,10 +61,10 @@ http {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

# Disable request buffering β€” required for streaming PUT uploads.
# Disable request buffering to allow streaming PUT uploads.
proxy_request_buffering off;

# Disable response buffering β€” stream GET bodies directly.
# Disable response buffering to stream GET responses directly.
proxy_buffering off;

# Pass all coordinator metadata headers through to client.
Expand All @@ -77,23 +74,21 @@ http {
proxy_pass_header Key-Volumes;
}

# ------------------------------------------------------------------
# Internal X-Accel-Redirect handler.
#
# URI format: /accel/<volume-host:port>/<object-path>
# Example: /accel/volume1:8080/sv09/a2/38/bXlib...
# Example: /accel/volume1:8080/sv09/a2/38/...
#
# `internal` makes this location unreachable by direct client
# requests β€” only X-Accel-Redirect from the coordinator can
# trigger it. Direct requests return 404.
# The `internal` directive prevents direct client access. Only
# X-Accel-Redirect responses from the coordinator can trigger this
# location. Direct requests return 404.
#
# The upstream is captured into a variable ($vol_upstream) so
# DNS resolution is deferred to request time (same pattern as above).
# ------------------------------------------------------------------
# The captured upstream is stored in a variable to defer DNS
# resolution to request time.
location ~ ^/accel/([^/]+)/(.*)$ {
internal;

# Capture volume host:port and path into variables for runtime DNS.
# Capture volume host:port and object path into variables.
set $vol_upstream $1;
set $vol_path $2;

Expand All @@ -102,26 +97,22 @@ http {
# Do not forward client request headers to volume servers.
proxy_pass_request_headers off;

# ---------------------------------------------------------------
# Content-Type injection via variable persistence.
# Content-Type handling.
#
# The coordinator sets X-Content-Type on its response.
# nginx stores this as $upstream_http_x_content_type β€” a variable
# that persists across the X-Accel-Redirect internal redirect
# (same ngx_http_request_t context).
# The coordinator provides X-Content-Type in its response. nginx
# exposes this as $upstream_http_x_content_type. This variable
# persists across the internal X-Accel-Redirect.
#
# If the coordinator has no stored Content-Type for this object
# (object was PUT without a Content-Type header, or rebuilt from
# volume data), $upstream_http_x_content_type will be empty.
# In that case we fall back to application/octet-stream rather
# than emitting an empty Content-Type header.
# If no Content-Type metadata exists (for example, the object was
# uploaded without one or reconstructed from volume data), the
# variable is empty. In that case, application/octet-stream is used.
#
# Objects can be re-PUT with Content-Type to populate the field.
# ---------------------------------------------------------------
# Objects may be re-uploaded with a Content-Type header to set
# the stored metadata.
proxy_hide_header Content-Type;

# Resolve effective Content-Type: coordinator metadata wins,
# fall back to octet-stream when metadata is absent.
# Coordinator metadata takes precedence. Fall back to
# application/octet-stream when absent.
set $effective_ct $upstream_http_x_content_type;
if ($effective_ct = "") {
set $effective_ct "application/octet-stream";
Expand Down
23 changes: 15 additions & 8 deletions config/nginx-volume.conf
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# Volume server configuration for minikv
#
# Requires: nginx-mod-http-dav-ext (installed via apk in Dockerfile.volume)
# Module path on Alpine 3.19: /usr/lib/nginx/modules/ngx_http_dav_ext_module.so
# Requires the nginx DAV extension module:
# nginx-mod-http-dav-ext
#
# All volume containers listen on 8080 internally.
# docker-compose maps volume1 => 8001, volume2 => 8002, volume3 => 8003 on the host.
# Alpine 3.19 module path:
# /usr/lib/nginx/modules/ngx_http_dav_ext_module.so
#
# daemon off is passed via CMD in Dockerfile.volume, not here, to avoid
# the duplicate-directive fatal error from some nginx base images.
# Each volume container listens on port 8080 internally.
# docker-compose maps:
# volume1 -> 8001
# volume2 -> 8002
# volume3 -> 8003
#
# `daemon off` is set via CMD in Dockerfile.volume to avoid duplicate
# directive errors in certain nginx base images.

load_module /usr/lib/nginx/modules/ngx_http_dav_ext_module.so;

Expand Down Expand Up @@ -43,13 +49,14 @@ http {
location / {
disable_symlinks off;

# Enable object writes and deletions via WebDAV.
dav_methods PUT DELETE;
dav_access group:rw all:r;

# Auto-creates parent shard directories on first PUT.
# Automatically create shard directory hierarchy on write.
create_full_put_path on;

# JSON directory listing β€” required by the rebuild subcommand.
# Expose JSON directory listings for rebuild operations.
autoindex on;
autoindex_format json;
}
Expand Down
Loading