Skip to content
Open
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
44 changes: 44 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Build artifacts
target/
**/target/

# Local environments
.venv/
venv/
__pycache__/
*.pyc
.pytest_cache/

# Editor / OS
.vscode/
.idea/
*.swp
.DS_Store

# Generated / runtime
*.log
discussions/
docs/pdfs/
docs/rendered/
pdfs/

# Local config (must NOT be baked into the image; mounted at /etc/extenddb)
extenddb.toml
external-suites.toml

# Tests are not needed for the runtime image. The Dockerfile build stage
# uses cargo workspace metadata only; integration tests run separately.
tests/

# Container artifacts (avoid recursive context inclusion)
# We DO want docker/entrypoint.sh in the context — only ignore the Dockerfile
# itself and the samples/docker dir, which holds compose and demo docs.
Dockerfile
.dockerignore
samples/docker/

# VCS
# .git/ is intentionally NOT ignored: crates/bin/build.rs runs `git rev-parse`
# during the build stage to bake the commit hash into the binary. Excluding
# .git would yield `commit unknown` in `extenddb version`.
.github/
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ pdfs/
docs/rendered/
discussions/
.DS_Store

# Generated by samples/docker/bootstrap-iam.sh
samples/docker/extenddb-creds.env
samples/docker/extenddb-cert.pem
115 changes: 115 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Copyright 2026 ExtendDB contributors
# SPDX-License-Identifier: Apache-2.0
#
# Multi-stage build for ExtendDB.
#
# Stage 1 (builder): cargo build --release --bin extenddb
# Stage 2 (runtime): debian:bookworm-slim with the static-ish binary
# plus tini, ca-certificates, and curl for healthchecks
#
# This image runs `extenddb serve` only. Operator runs `extenddb init`
# explicitly as a separate one-shot before first start. See
# samples/docker/README.md for the bootstrap walkthrough.
#
# Build:
# docker build -t extenddb:dev .
#
# Run (after init):
# docker run --rm -p 8000:8000 \
# -v extenddb-config:/etc/extenddb \
# -v extenddb-state:/var/lib/extenddb \
# extenddb:dev

# ---- Stage 1: builder ----
FROM rust:1.88-bookworm AS builder

WORKDIR /src

# Install git so build.rs can read the commit hash.
# pkg-config and libssl-dev are NOT required: ExtendDB uses rustls.
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Copy the entire workspace. .dockerignore strips target/, tests/, docs/,
# samples/, devtools/, .venv/, etc.
COPY . .

# Cache cargo registry across builds via BuildKit.
# Build only the binary; library crates are pulled in transitively.
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/src/target \
cargo build --release --bin extenddb \
&& cp /src/target/release/extenddb /usr/local/bin/extenddb \
&& /usr/local/bin/extenddb --version

# ---- Stage 2: runtime ----
FROM debian:bookworm-slim AS runtime

ARG EXTENDDB_UID=1000
ARG EXTENDDB_GID=1000

# Runtime dependencies:
# ca-certificates: for outbound TLS (e.g. to RDS). Server-side TLS uses rustls and needs no system roots.
# tini: PID 1 reaper, signal forwarding.
# curl: HEALTHCHECK uses it to hit /health. ~250 KB.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
tini \
curl \
&& rm -rf /var/lib/apt/lists/*

# Non-root user. HOME=/var/lib/extenddb so init resolves ~/.extenddb
# to a writeable, container-friendly state directory.
RUN groupadd --system --gid ${EXTENDDB_GID} extenddb \
&& useradd --system --uid ${EXTENDDB_UID} --gid extenddb \
--home-dir /var/lib/extenddb --shell /usr/sbin/nologin extenddb \
&& mkdir -p /etc/extenddb /var/lib/extenddb \
&& chown -R extenddb:extenddb /etc/extenddb /var/lib/extenddb \
&& chmod 0750 /etc/extenddb /var/lib/extenddb

COPY --from=builder /usr/local/bin/extenddb /usr/local/bin/extenddb
COPY docker/entrypoint.sh /usr/local/bin/extenddb-entrypoint
RUN chmod 0755 /usr/local/bin/extenddb /usr/local/bin/extenddb-entrypoint

USER extenddb
WORKDIR /var/lib/extenddb
ENV HOME=/var/lib/extenddb

# Default DynamoDB API port. Operator can override with EXTENDDB__SERVER__PORT
# at serve time and remap the published port.
EXPOSE 8000

# Container runtimes default to SIGTERM, but stating it explicitly is a
# defence against future Docker default changes. extenddb honors SIGTERM
# (the entrypoint forwards it to the daemon).
STOPSIGNAL SIGTERM

# State volumes:
# /etc/extenddb: owns extenddb.toml (written by `init`, read by `serve`)
# /var/lib/extenddb: owns ~/.extenddb/{tls,run} state
VOLUME ["/etc/extenddb", "/var/lib/extenddb"]

# Healthcheck: curl with -k since the cert is self-signed by default.
# Operators with a CA-signed cert can override at run time.
HEALTHCHECK --interval=10s --timeout=3s --start-period=20s --retries=3 \
CMD curl -kfsS https://127.0.0.1:8000/health || exit 1

# tini reaps zombies and forwards signals to the entrypoint script.
# Default CMD is "serve"; pass any other extenddb subcommand (e.g.
# docker run ... extenddb init --pg-host postgres ...
# ) and the entrypoint execs it directly.
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/extenddb-entrypoint"]
CMD ["serve"]

# OCI labels (org.opencontainers.image.*). The CI workflow overrides these
# via docker/metadata-action with revision/version/created at publish time;
# the values here are the static fallback for local builds.
LABEL org.opencontainers.image.title="ExtendDB" \
org.opencontainers.image.description="DynamoDB-compatible API server backed by PostgreSQL" \
org.opencontainers.image.url="https://github.com/ExtendDB/extenddb" \
org.opencontainers.image.source="https://github.com/ExtendDB/extenddb" \
org.opencontainers.image.documentation="https://github.com/ExtendDB/extenddb/tree/main/docs" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.vendor="ExtendDB contributors"
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ scripts/install-linux.sh # Linux
scripts/install-macos.sh # macOS
```

### Run with Docker

A Docker Compose stack brings up PostgreSQL and ExtendDB end-to-end:

```bash
cd samples/docker
docker compose -f compose.yaml -f compose.dev.yaml up --build -d
./bootstrap-iam.sh # creates an IAM user + access key
source ./extenddb-creds.env
aws dynamodb list-tables --endpoint-url "$EXTENDDB_ENDPOINT"
```

See [`samples/docker/README.md`](samples/docker/README.md) for the full
walkthrough.

## Prerequisites

- Rust 1.85+ (`rustup update`)
Expand Down
115 changes: 115 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/bin/sh
# Copyright 2026 ExtendDB contributors
# SPDX-License-Identifier: Apache-2.0
#
# Container entrypoint for ExtendDB.
#
# Argument handling:
# 1. If the first arg is "extenddb", strip it. This lets users copy-paste
# `extenddb <subcommand>` invocations from docs without surprise:
# docker run <image> extenddb init ...
# docker run <image> init ...
# both work and behave identically.
# 2. If the (remaining) first arg is "serve" or empty (the default CMD),
# run `extenddb serve` and wait on the daemon.
# 3. Otherwise, exec `extenddb "$@"`. Used for init, manage, status, etc.
#
# `extenddb serve` always forks into the background. Without this wrapper
# the container would exit immediately after the parent process returns.
# Once a foreground/no-detach mode lands upstream, this whole script
# collapses to `exec extenddb "$@"`.

set -eu

CONFIG="${EXTENDDB_CONFIG:-/etc/extenddb/extenddb.toml}"
STATE_DIR="${HOME:-/var/lib/extenddb}/.extenddb"
RUN_DIR="${STATE_DIR}/run"

# Strip an optional leading "extenddb" arg so both invocation styles work.
if [ "${1:-}" = "extenddb" ]; then
shift
fi

# Anything other than "serve" (or no args) goes straight to the binary.
case "${1:-serve}" in
serve) ;; # fall through to the serve path below
*) exec extenddb "$@" ;;
esac

# --- serve path ---

if [ ! -f "$CONFIG" ]; then
cat >&2 <<EOF
extenddb-entrypoint: config file not found at $CONFIG.

This image does not auto-initialize. Run \`init\` first, e.g.:

docker run --rm \\
-v extenddb-config:/etc/extenddb \\
-v extenddb-state:/var/lib/extenddb \\
<image> init \\
--config $CONFIG \\
--pg-host <postgres-host> --pg-user <user> --pg-pass <pass> \\
--bind-addr 0.0.0.0

See samples/docker/README.md for the full bootstrap walkthrough.
EOF
exit 1
fi

extenddb serve --config "$CONFIG"

# Install the signal-forwarding trap BEFORE waiting for the PID file.
# `extenddb serve` returns to the parent immediately after the double-fork;
# the daemon may take up to ~15 s on a slow runner to write its PID file.
# A SIGTERM landing in that window must still trigger graceful shutdown,
# so the trap tolerates an unset $PID and falls through to the polling
# loop below (which will exit naturally once the daemon is running and
# then dies, or once the wait loop times out).
#
# Why not `tail --pid`? It blocks but does not forward signals. SIGTERM
# to the container would reach `tail`, kill it, and orphan the daemon.
# The container runtime would then fall through to SIGKILL after the
# grace period, skipping graceful shutdown.
PID=""
shutdown() {
if [ -n "${PID:-}" ]; then
echo "extenddb-entrypoint: forwarding ${1:-TERM} to daemon pid $PID"
kill -"${1:-TERM}" "$PID" 2>/dev/null || true
else
echo "extenddb-entrypoint: ${1:-TERM} received before daemon PID was known; exiting"
exit 143
fi
}
trap 'shutdown TERM' TERM
trap 'shutdown INT' INT

# extenddb writes a PID file at $run_dir/extenddb-$port.pid. The port comes
# from the config file, which the operator may have changed, so we glob.
# Wait up to ~15 s for the daemon to write its PID. The daemon normally
# writes within ~200 ms but slow CI runners and cold caches can stretch it.
i=0
while [ "$i" -lt 30 ]; do
PID_FILE="$(ls "${RUN_DIR}"/extenddb-*.pid 2>/dev/null | head -n 1 || true)"
if [ -n "$PID_FILE" ] && [ -f "$PID_FILE" ]; then
break
fi
sleep 0.5
i=$((i + 1))
done

if [ -z "${PID_FILE:-}" ] || [ ! -f "$PID_FILE" ]; then
echo "extenddb-entrypoint: daemon failed to start (no PID file under $RUN_DIR)" >&2
exit 1
fi

PID="$(cat "$PID_FILE")"
echo "extenddb-entrypoint: daemon started (pid $PID, pid-file $PID_FILE)"

# Poll on the daemon. kill -0 returns 0 while the process is alive.
# 500 ms granularity keeps shutdown responsive without burning CPU.
while kill -0 "$PID" 2>/dev/null; do
sleep 0.5
done

echo "extenddb-entrypoint: daemon (pid $PID) exited"
Loading