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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ repository = "https://github.com/zingolabs"
homepage = "https://www.zingolabs.org/"
edition = "2021"
license = "Apache-2.0"
version = "0.1.2"
version = "0.2.0"


[workspace.dependencies]
Expand Down
34 changes: 22 additions & 12 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
fi

############################
# Runtime (slim, non-root)
# Runtime
############################
FROM debian:bookworm-slim AS runtime
SHELL ["/bin/bash", "-euo", "pipefail", "-c"]
Expand All @@ -50,33 +50,43 @@ ARG GID
ARG USER
ARG HOME

# Only the dynamic libs needed by a Rust/OpenSSL binary
# Runtime deps + setpriv for privilege dropping
RUN apt-get -qq update && \
apt-get -qq install -y --no-install-recommends \
ca-certificates libssl3 libgcc-s1 \
ca-certificates libssl3 libgcc-s1 util-linux \
&& rm -rf /var/lib/apt/lists/*

# Create non-root user
# Create non-root user (entrypoint will drop privileges to this user)
RUN addgroup --gid "${GID}" "${USER}" && \
adduser --uid "${UID}" --gid "${GID}" --home "${HOME}" \
--disabled-password --gecos "" "${USER}"

# Make UID/GID available to entrypoint
ENV UID=${UID} GID=${GID} HOME=${HOME}

WORKDIR ${HOME}

# Copy the installed binary from builder
COPY --from=builder /out/bin/zainod /usr/local/bin/zainod
# Create ergonomic mount points with symlinks to XDG defaults
# Users mount to /app/config and /app/data, zaino uses ~/.config/zaino and ~/.cache/zaino
RUN mkdir -p /app/config /app/data && \
mkdir -p ${HOME}/.config ${HOME}/.cache && \
ln -s /app/config ${HOME}/.config/zaino && \
ln -s /app/data ${HOME}/.cache/zaino && \
chown -R ${UID}:${GID} /app ${HOME}/.config ${HOME}/.cache

RUN mkdir -p .cache/zaino
RUN chown -R "${UID}:${GID}" "${HOME}"
USER ${USER}
# Copy binary and entrypoint
COPY --from=builder /out/bin/zainod /usr/local/bin/zainod
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# Default ports (adjust if your app uses different ones)
# Default ports
ARG ZAINO_GRPC_PORT=8137
ARG ZAINO_JSON_RPC_PORT=8237
EXPOSE ${ZAINO_GRPC_PORT} ${ZAINO_JSON_RPC_PORT}

# Healthcheck that doesn't assume specific HTTP/gRPC endpoints
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD /usr/local/bin/zainod --version >/dev/null 2>&1 || exit 1

CMD ["zainod"]
# Start as root; entrypoint drops privileges after setting up directories
ENTRYPOINT ["/entrypoint.sh"]
CMD ["start"]
136 changes: 136 additions & 0 deletions docs/docker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Docker Usage

This document covers running Zaino using the official Docker image.

## Overview

The Docker image runs `zainod` - the Zaino indexer daemon. The image:

- Uses `zainod` as the entrypoint with `start` as the default subcommand
- Runs as non-root user (`container_user`, UID 1000) after initial setup
- Handles volume permissions automatically for default paths

For CLI usage details, see the CLI documentation or run `docker run --rm zaino --help`.

## Configuration Options

The container can be configured via:

1. **Environment variables only** - Suitable for simple deployments, but sensitive fields (passwords, secrets, tokens, cookies, private keys) cannot be set via env vars for security reasons
2. **Config file + env vars** - Mount a config file for sensitive fields, override others with env vars
3. **Config file only** - Mount a complete config file

For data persistence, volume mounts are recommended for the database/cache directory.

## Deployment with Docker Compose

The recommended way to run Zaino is with Docker Compose, typically alongside Zebra:

```yaml
services:
zaino:
image: zaino:latest
ports:
- "8137:8137" # gRPC
- "8237:8237" # JSON-RPC (if enabled)
volumes:
- ./config:/app/config:ro
- zaino-data:/app/data
environment:
- ZAINO_VALIDATOR_SETTINGS__VALIDATOR_JSONRPC_LISTEN_ADDRESS=zebra:18232
depends_on:
- zebra

zebra:
image: zfnd/zebra:latest
volumes:
- zebra-data:/home/zebra/.cache/zebra
# ... zebra configuration

volumes:
zaino-data:
zebra-data:
```

If Zebra runs on a different host/network, adjust `VALIDATOR_JSONRPC_LISTEN_ADDRESS` accordingly.

## Initial Setup: Generating Configuration

To generate a config file on your host for customization:

```bash
mkdir -p ./config

docker run --rm -v ./config:/app/config zaino generate-config

# Config is now at ./config/zainod.toml - edit as needed
```

## Container Paths

The container provides simple mount points:

| Purpose | Mount Point |
|---------|-------------|
| Config | `/app/config` |
| Database | `/app/data` |

These are symlinked internally to the XDG paths that Zaino expects.

## Volume Permission Handling

The entrypoint handles permissions automatically:

1. Container starts as root
2. Creates directories and sets ownership to UID 1000
3. Drops privileges and runs `zainod`

This means you can mount volumes without pre-configuring ownership.

### Read-Only Config Mounts

Config files can (and should) be mounted read-only:

```yaml
volumes:
- ./config:/app/config:ro
```

## Configuration via Environment Variables

Config values can be set via environment variables prefixed with `ZAINO_`, using `__` for nesting:

```yaml
environment:
- ZAINO_NETWORK=Mainnet
- ZAINO_VALIDATOR_SETTINGS__VALIDATOR_JSONRPC_LISTEN_ADDRESS=zebra:18232
- ZAINO_GRPC_SETTINGS__LISTEN_ADDRESS=0.0.0.0:8137
```

### Sensitive Fields

For security, the following fields **cannot** be set via environment variables and must use a config file:

- `*_password` (e.g., `validator_password`)
- `*_secret`
- `*_token`
- `*_cookie`
- `*_private_key`

If you attempt to set these via env vars, Zaino will error on startup.

## Health Check

The image includes a health check:

```bash
docker inspect --format='{{.State.Health.Status}}' <container>
```

## Local Testing

Permission handling can be tested locally:

```bash
./test_environment/test-docker-permissions.sh zaino:latest
```
76 changes: 76 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env bash

# Entrypoint for running Zaino in Docker.
#
# This script handles privilege dropping and directory setup for default paths.
# Configuration is managed by config-rs using defaults, optional TOML, and
# environment variables prefixed with ZAINO_.
#
# NOTE: This script only handles directories specified via environment variables
# or the defaults below. If you configure custom paths in a TOML config file,
# you are responsible for ensuring those directories exist with appropriate
# permissions before starting the container.

set -eo pipefail

# Default writable paths.
# The Dockerfile creates symlinks: /app/config -> ~/.config/zaino, /app/data -> ~/.cache/zaino
# So we handle /app/* paths directly for Docker users.
#
# Database path (symlinked from ~/.cache/zaino)
: "${ZAINO_STORAGE__DATABASE__PATH:=/app/data}"
#
# Cookie dir (runtime, ephemeral)
: "${ZAINO_JSON_SERVER_SETTINGS__COOKIE_DIR:=${XDG_RUNTIME_DIR:-/tmp}/zaino}"
#
# Config directory (symlinked from ~/.config/zaino)
ZAINO_CONFIG_DIR="/app/config"

# Drop privileges and execute command as non-root user
exec_as_user() {
user=$(id -u)
if [[ ${user} == '0' ]]; then
exec setpriv --reuid="${UID}" --regid="${GID}" --init-groups "$@"
else
exec "$@"
fi
}

exit_error() {
echo "ERROR: $1" >&2
exit 1
}

# Creates a directory if it doesn't exist and sets ownership to UID:GID.
# Gracefully handles read-only mounts by skipping chown if it fails.
create_owned_directory() {
local dir="$1"
[[ -z ${dir} ]] && return

# Try to create directory; skip if read-only
if ! mkdir -p "${dir}" 2>/dev/null; then
echo "WARN: Cannot create ${dir} (read-only or permission denied), skipping"
return 0
fi

# Try to set ownership; skip if read-only
if ! chown -R "${UID}:${GID}" "${dir}" 2>/dev/null; then
echo "WARN: Cannot chown ${dir} (read-only?), skipping"
return 0
fi

# Set ownership on parent if it's not root or home
local parent_dir
parent_dir="$(dirname "${dir}")"
if [[ "${parent_dir}" != "/" && "${parent_dir}" != "${HOME}" ]]; then
chown "${UID}:${GID}" "${parent_dir}" 2>/dev/null || true
fi
}

# Create and set ownership on writable directories
create_owned_directory "${ZAINO_STORAGE__DATABASE__PATH}"
create_owned_directory "${ZAINO_JSON_SERVER_SETTINGS__COOKIE_DIR}"
create_owned_directory "${ZAINO_CONFIG_DIR}"

# Execute zainod with dropped privileges
exec_as_user zainod "$@"
Loading
Loading