Dockerfile
Rama main · réplica de lectura del repositorio original.
# ====================
# Stage 1: Build nsjail from source
# ====================
FROM debian:bookworm-slim AS nsjail-builder
ENV DEBIAN_FRONTEND=noninteractive
# Pin NsJail 4.0 source with the in-process NSTUN policy backend.
ENV NSJAIL_COMMIT=388b9655a696e88df3185d09e36c0378a8532904
RUN apt-get update && apt-get install -y --no-install-recommends \
git gcc g++ make pkg-config bison flex \
libprotobuf-dev protobuf-compiler libnl-route-3-dev ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY docker/nsjail/nstun-bounded-memory.patch /tmp/nstun-bounded-memory.patch
RUN git clone https://github.com/google/nsjail.git /tmp/nsjail && \
cd /tmp/nsjail && git checkout "${NSJAIL_COMMIT}" && \
git apply --check /tmp/nstun-bounded-memory.patch && \
git apply /tmp/nstun-bounded-memory.patch && \
git submodule update --init --recursive && \
make -j"$(nproc)" && \
install -m 0755 nsjail /usr/local/bin/nsjail && \
rm -rf /tmp/nsjail
COPY docker/loop-device-sync.cc /tmp/loop-device-sync.cc
RUN g++ -std=c++20 -O2 -Wall -Wextra -Werror -Wformat=2 \
-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE -pie \
-Wl,-z,relro,-z,now \
/tmp/loop-device-sync.cc -o /usr/local/bin/tracecat-loop-device-sync && \
strip /usr/local/bin/tracecat-loop-device-sync && \
rm /tmp/loop-device-sync.cc
# ====================
# Stage 2: Create minimal sandbox rootfs
# ====================
FROM node:22.23.4-slim AS node-bin
FROM python:3.12-slim-bookworm AS sandbox-rootfs
ARG TARGETARCH
ARG DUCKDB_VERSION=2.0.0
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl wget jq iputils-ping git openssh-client squashfs-tools \
&& rm -rf /var/lib/apt/lists/*
# The slim base ships the stdlib without bytecode, and sandboxes mount this
# rootfs read-only, so every sandboxed Python process would recompile the
# stdlib from source. Checked hash-based .pyc files stay valid whatever mtimes
# the later COPY steps leave on the sources. Python still hashes each source
# on import, so an image built on this one that patches a stdlib module never
# runs stale bytecode.
RUN python -m compileall -q -j0 --invalidation-mode checked-hash \
"$(python -c 'import sysconfig; print(sysconfig.get_path("stdlib"))')"
# This rootfs is shared by run_python and agent sandboxes; CLI additions here
# are intentionally available to both. DuckDB is not available from Bookworm
# apt, so install the official release binary, verify it, preinstall extensions
# (json is built in since 2.0), and wrap the CLI to load those extensions on
# each invocation.
#
# Both the CLI binary and every extension are pinned by sha256: extensions are
# downloaded from the version-pinned repository, verified, and installed from
# local files (DuckDB still validates the signed extensions on load) instead of
# resolving "latest for this version" via a bare INSTALL.
RUN set -eu; \
arch="${TARGETARCH:-$(dpkg --print-architecture)}"; \
case "${arch}" in \
amd64) \
platform="linux_amd64"; \
duckdb_sha256="c6507206baf7793c6013dbd82368469a45f20dc0e17a6f92f204602f652ed2bc"; \
ext_shas="httpfs:ee6c7d7e60a5da1b6f6bf6156b4298b55ddce0496a5bfe628bd6ab6d47eb8e3e inet:afc4efe65dfb5106b18e3ab75753f680bd6eecc5d17ec347a325b3bf0e55cfdd fts:9172520652ce609d163cf30645f382b197f2c7294448a816c77d62b0f029d96e"; \
;; \
arm64) \
platform="linux_arm64"; \
duckdb_sha256="859e1875d2a9515fbb4bf8871061437a14af82320da68c753f094c4b776348fe"; \
ext_shas="httpfs:f524888fc7d74cc0c6c4a8f7ed5beedac5b12e71cb426503f882c72ce6afbd43 inet:fa189a8adaa657299d05b8f99a181b10223f5063c396514dea06f8219ff646ab fts:d233d2311825da4d9b6efb45edd52b126b18134c7d7083b1e58f6d450ebdabee"; \
;; \
*) echo "Unsupported DuckDB CLI architecture: ${arch}" >&2; exit 1 ;; \
esac; \
curl -fsSL "https://github.com/duckdb/duckdb/releases/download/v${DUCKDB_VERSION}/duckdb-cli-linux-${arch}.tar.gz" -o /tmp/duckdb.tar.gz; \
echo "${duckdb_sha256} /tmp/duckdb.tar.gz" | sha256sum -c -; \
tar -xzf /tmp/duckdb.tar.gz -C /tmp duckdb; \
rm -f /tmp/duckdb.tar.gz; \
install -m 0755 /tmp/duckdb /usr/local/bin/duckdb.real; \
rm -f /tmp/duckdb; \
mkdir -p /usr/local/lib/duckdb/extensions /usr/local/share/duckdb /tmp/ddbext; \
install_args=""; \
for spec in ${ext_shas}; do \
name="${spec%%:*}"; sha="${spec##*:}"; \
curl -fsSL "https://extensions.duckdb.org/v${DUCKDB_VERSION}/${platform}/${name}.duckdb_extension.gz" -o "/tmp/ddbext/${name}.duckdb_extension.gz"; \
echo "${sha} /tmp/ddbext/${name}.duckdb_extension.gz" | sha256sum -c -; \
gunzip "/tmp/ddbext/${name}.duckdb_extension.gz"; \
install_args="${install_args} INSTALL '/tmp/ddbext/${name}.duckdb_extension';"; \
done; \
/usr/local/bin/duckdb.real -c "SET extension_directories = ['/usr/local/lib/duckdb/extensions'];${install_args}"; \
rm -rf /tmp/ddbext; \
printf '%s\n' \
"SET extension_directories = ['/usr/local/lib/duckdb/extensions'];" \
"LOAD httpfs;" \
"LOAD inet;" \
"LOAD fts;" \
> /usr/local/share/duckdb/tracecat-init.sql; \
printf '%s\n' \
'#!/bin/sh' \
'exec /usr/local/bin/duckdb.real -init /usr/local/share/duckdb/tracecat-init.sql "$@"' \
> /usr/local/bin/duckdb; \
chmod 0755 /usr/local/bin/duckdb; \
jq --version; \
duckdb --version; \
test "$(duckdb -csv -noheader -c "SELECT count(*) FROM duckdb_extensions() WHERE extension_name IN ('json', 'httpfs', 'inet', 'fts') AND installed AND loaded;")" = "4"
COPY --from=ghcr.io/astral-sh/uv:0.13.4 /uv /usr/local/bin/uv
COPY --from=ghcr.io/astral-sh/uv:0.13.4 /uvx /usr/local/bin/uvx
COPY --from=node-bin /usr/local/bin/node /usr/local/bin/node
COPY --from=node-bin /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
RUN useradd -m -u 1000 sandbox && \
mkdir -p /workspace /work /cache /packages /home/sandbox && \
chown sandbox:sandbox /workspace /work /cache /packages /home/sandbox
ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
# ====================
# Stage 3: Shared base with runtime dependencies
# ====================
FROM python:3.12-slim-bookworm AS base
COPY --from=ghcr.io/astral-sh/uv:0.13.4 /uv /uvx /usr/local/bin/
ENV HOST=0.0.0.0 PORT=8000
# Copy nsjail binary
COPY --from=nsjail-builder /usr/local/bin/nsjail /usr/local/bin/nsjail
# Copy Node.js + npx for in-process MCP command servers (stdio)
COPY --from=node-bin /usr/local/bin/node /usr/local/bin/node
COPY --from=node-bin /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
# Install runtime packages
RUN apt-get update && apt-get install -y --no-install-recommends \
acl git openssh-client xmlsec1 libmagic1 curl ca-certificates jq \
libnl-route-3-200 libprotobuf32 libcap2-bin util-linux \
squashfs-tools \
&& apt-get -y upgrade \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# DuckDB (CLI binary + extensions) is stored as a single physical copy inside
# the sandbox rootfs (copied in below). The executor host — including the
# in-process DuckDB Python package used by core.duckdb.execute_sql — reaches it
# through symlinks created right after that copy, so the large extension
# binaries are not stored twice in the image. The Python package loads
# extensions from this directory instead of autoinstalling over the network.
ENV TRACECAT__DUCKDB_EXTENSION_DIRECTORY=/usr/local/lib/duckdb/extensions
# Copy sandbox rootfs. --chmod=755 normalizes every copied path and drops
# setuid/setgid bits at copy time; a recursive chmod after the copy would store
# a second full copy of the rootfs in its own layer.
COPY --from=sandbox-rootfs --chmod=755 /usr /var/lib/tracecat/sandbox-rootfs/usr
# Expose the single rootfs DuckDB copy to the executor host via symlinks, so the
# host CLI and the in-process DuckDB Python package work without a second copy
# of the extension binaries. The nsjail sandbox uses its own (physical) rootfs
# copy and is unaffected by these host-side links.
RUN ln -s /var/lib/tracecat/sandbox-rootfs/usr/local/lib/duckdb /usr/local/lib/duckdb && \
ln -s /var/lib/tracecat/sandbox-rootfs/usr/local/share/duckdb /usr/local/share/duckdb && \
ln -s /var/lib/tracecat/sandbox-rootfs/usr/local/bin/duckdb.real /usr/local/bin/duckdb.real && \
ln -s /var/lib/tracecat/sandbox-rootfs/usr/local/bin/duckdb /usr/local/bin/duckdb && \
jq --version && duckdb --version
COPY --from=sandbox-rootfs --chmod=755 /lib /var/lib/tracecat/sandbox-rootfs/lib
COPY --from=sandbox-rootfs --chmod=755 /bin /var/lib/tracecat/sandbox-rootfs/bin
COPY --from=sandbox-rootfs --chmod=755 /sbin /var/lib/tracecat/sandbox-rootfs/sbin
COPY --from=sandbox-rootfs --chmod=755 /etc/passwd /var/lib/tracecat/sandbox-rootfs/etc/passwd
COPY --from=sandbox-rootfs --chmod=755 /etc/group /var/lib/tracecat/sandbox-rootfs/etc/group
COPY --from=sandbox-rootfs --chmod=755 /etc/ssl /var/lib/tracecat/sandbox-rootfs/etc/ssl
COPY --from=sandbox-rootfs --chmod=755 /etc/ca-certificates /var/lib/tracecat/sandbox-rootfs/etc/ca-certificates
RUN install -m 0644 /dev/null /var/lib/tracecat/sandbox-rootfs/etc/resolv.conf && \
install -m 0644 /dev/null /var/lib/tracecat/sandbox-rootfs/etc/hosts && \
install -m 0644 /dev/null /var/lib/tracecat/sandbox-rootfs/etc/nsswitch.conf
# Handle lib64 for amd64
RUN if [ -d /var/lib/tracecat/sandbox-rootfs/lib64 ] || [ "$(uname -m)" = "x86_64" ]; then \
mkdir -p /var/lib/tracecat/sandbox-rootfs/lib64 && \
cp -a /lib64/. /var/lib/tracecat/sandbox-rootfs/lib64/ 2>/dev/null || true; \
fi
# Create sandbox directories
RUN mkdir -p /var/lib/tracecat/sandbox-rootfs/tmp \
/var/lib/tracecat/sandbox-rootfs/proc \
/var/lib/tracecat/sandbox-rootfs/dev \
/var/lib/tracecat/sandbox-rootfs/work \
/var/lib/tracecat/sandbox-rootfs/cache \
/var/lib/tracecat/sandbox-rootfs/packages \
/var/lib/tracecat/sandbox-rootfs/home/sandbox \
/var/lib/tracecat/sandbox-cache/packages \
/var/lib/tracecat/sandbox-cache/uv-cache && \
chown -R 1000:1000 /var/lib/tracecat/sandbox-rootfs/work \
/var/lib/tracecat/sandbox-rootfs/cache \
/var/lib/tracecat/sandbox-rootfs/packages \
/var/lib/tracecat/sandbox-rootfs/home/sandbox && \
chmod 1777 /var/lib/tracecat/sandbox-rootfs/tmp
# Create apiuser for the non-root runtime.
RUN groupadd -g 1001 apiuser && useradd -m -u 1001 -g apiuser apiuser && \
mkdir -p /home/apiuser/.cache/uv /home/apiuser/.cache/s3 /home/apiuser/.cache/tmp /home/apiuser/.local/bin && \
chown -R apiuser:apiuser /home/apiuser
# Create MCP socket directory for apiuser
RUN mkdir -p /var/run/tracecat && chown 1001:1001 /var/run/tracecat
# Allow the non-root executor process to invoke mount/umount and synchronize
# kernel-confirmed loop nodes when the runtime grants the needed bounding
# capabilities. The fixed-purpose sync helper cannot create arbitrary devices.
COPY --from=nsjail-builder /usr/local/bin/tracecat-loop-device-sync /usr/local/bin/tracecat-loop-device-sync
RUN chmod u-s /usr/bin/mount /usr/bin/umount && \
setcap cap_sys_admin,cap_dac_override+ep /usr/bin/mount && \
setcap cap_sys_admin,cap_dac_override+ep /usr/bin/umount && \
setcap cap_mknod,cap_dac_override,cap_setuid+ep /usr/local/bin/tracecat-loop-device-sync
WORKDIR /app
# ====================
# Stage 4: Fetch workspace-chat copilot skills
# ====================
FROM base AS plugin-skills
ARG TRACECAT_PLUGINS_REF=0140783181f0afcb69961661fa1474402ca46a4e
ARG TRACECAT_PLUGINS_ARCHIVE_SHA256=408928d337e7330d10fd099d07c818d724def925eb6b77eaf0cb6ace2777f5dd
# Pinned to a tracecat-plugins commit on main. Bump both values when the
# vendored skills change so the trusted payload is verified before extraction.
RUN set -eux; \
mkdir -p /skills /tmp/tracecat-plugins; \
curl -fsSL \
"https://github.com/TracecatHQ/tracecat-plugins/archive/${TRACECAT_PLUGINS_REF}.tar.gz" \
-o /tmp/tracecat-plugins.tar.gz; \
echo "${TRACECAT_PLUGINS_ARCHIVE_SHA256} /tmp/tracecat-plugins.tar.gz" \
| sha256sum -c -; \
tar -xzf /tmp/tracecat-plugins.tar.gz \
-C /tmp/tracecat-plugins \
--strip-components=1; \
for skill in \
tracecat-workspace-chat \
tracecat-automation-best-practices \
tracecat-slackbot-best-practices; do \
source="/tmp/tracecat-plugins/plugins/tracecat/skills/${skill}"; \
test -d "${source}"; \
test -f "${source}/SKILL.md"; \
cp -a "${source}" /skills/; \
done; \
test "$(find /skills -mindepth 1 -maxdepth 1 -type d | wc -l)" -eq 3; \
rm -rf /tmp/tracecat-plugins /tmp/tracecat-plugins.tar.gz
# Workspace Chat reads platform guidance from docs built from this same commit.
# Keep source structure so docs.json and MDX imports remain useful navigation.
COPY ./docs /tmp/tracecat-docs
RUN set -eux; \
docs_source=/tmp/tracecat-docs; \
docs_target=/skills/tracecat-workspace-chat/references/docs; \
mkdir -p "${docs_target}"; \
cd "${docs_source}"; \
find . -type f \( \
-name '*.mdx' -o \
-name 'docs.json' -o \
-path './automations/core-actions/_examples.yaml' -o \
-path './automations/core-actions/_manifest.yaml' \
\) -exec cp --parents '{}' "${docs_target}" \;; \
source_count="$(find "${docs_source}" -type f \( \
-name '*.mdx' -o \
-name 'docs.json' -o \
-path '*/automations/core-actions/_examples.yaml' -o \
-path '*/automations/core-actions/_manifest.yaml' \
\) | wc -l)"; \
target_count="$(find "${docs_target}" -type f | wc -l)"; \
test "${source_count}" -gt 0; \
test "${source_count}" -eq "${target_count}"; \
test -f "${docs_target}/docs.json"; \
test -f "${docs_target}/agents/workspace-chat.mdx"; \
test -f "${docs_target}/automations/workflows.mdx"; \
test -f "${docs_target}/automations/core-actions/_examples.yaml"; \
test -f "${docs_target}/automations/core-actions/_manifest.yaml"; \
test -z "$(find "${docs_target}" -type f \( \
-iname '*.gif' -o -iname '*.jpeg' -o -iname '*.jpg' -o \
-iname '*.png' -o -iname '*.svg' -o -iname '*.webp' \
\) -print -quit)"; \
rm -rf "${docs_source}"
# ====================
# Stage 5: Prepare development source without bundled docs
# ====================
FROM base AS development-source
COPY . /source/
RUN rm -rf /source/docs
# ====================
# Stage 6: Development app
# ====================
FROM base AS development-app
ENV TMPDIR="/home/apiuser/.cache/tmp" TEMP="/home/apiuser/.cache/tmp" TMP="/home/apiuser/.cache/tmp"
# Set sandbox cache permissions for apiuser
RUN chown -R 1001:1001 /var/lib/tracecat/sandbox-cache && \
chmod -R 755 /var/lib/tracecat/sandbox-cache
RUN chown apiuser:apiuser /app
# Switch to the non-root user used in production before installing, so /app
# never needs a recursive chown (which would store a second copy of the venv).
USER apiuser
# Install third-party dependencies only, so this layer is reused until uv.lock or
# pyproject.toml change. --frozen because uv cannot validate the lockfile
# without workspace member sources; the sync below runs with --locked.
RUN --mount=type=cache,target=/home/apiuser/.cache/uv,uid=1001,gid=1001 \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-workspace --no-dev --group admin --no-editable
COPY --from=development-source --chown=apiuser:apiuser /source/ /app/
COPY --from=plugin-skills --chown=apiuser:apiuser /skills/ /var/lib/tracecat/copilot-skills/
RUN --mount=type=cache,target=/home/apiuser/.cache/uv,uid=1001,gid=1001 uv sync --locked --no-dev --group admin
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONPATH="/home/apiuser/.local"
RUN mkdir -p /home/apiuser/.local/bin && ln -s $(which uv) /home/apiuser/.local/bin/uv
EXPOSE $PORT
CMD ["sh", "-c", "python3 -m uvicorn tracecat.api.app:app --host $HOST --port $PORT --reload"]
# ====================
# Stage 7: Development registry manifest
# ====================
FROM development-app AS development-registry-manifest
RUN /app/.venv/bin/python -m tracecat.registry.sync.prebuild
# ====================
# Stage 8: Development target
# ====================
FROM development-app AS development
# Carry only the generated builtin registry metadata in dev images too, so local
# cluster startup can update the DB without rediscovering actions on the hot path.
COPY --from=development-registry-manifest --chown=apiuser:apiuser /app/.registry-artifacts /app/.registry-artifacts
# ====================
# Stage 9: Test target (development + pytest)
# ====================
FROM development AS test
# Install test dependencies
RUN --mount=type=cache,target=/home/apiuser/.cache/uv,uid=1001,gid=1001 uv sync --frozen --group dev --group admin
CMD ["python", "-m", "pytest"]
# ====================
# Stage 10: Production app
# ====================
FROM base AS production-app
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
# Set sandbox cache permissions for apiuser
RUN chown -R 1001:1001 /var/lib/tracecat/sandbox-cache && \
chmod -R 755 /var/lib/tracecat/sandbox-cache
ENV PYTHONUSERBASE="/home/apiuser/.local"
ENV UV_CACHE_DIR="/home/apiuser/.cache/uv"
ENV PYTHONPATH="/home/apiuser/.local"
ENV PATH="/home/apiuser/.local/bin:/usr/local/bin:/usr/bin:/bin"
ENV TMPDIR="/home/apiuser/.cache/tmp" TEMP="/home/apiuser/.cache/tmp" TMP="/home/apiuser/.cache/tmp"
RUN mkdir -p /app/.scripts && chown -R apiuser:apiuser /app
# Switch to non-root user
USER apiuser
# Install third-party dependencies only, so this layer is reused until uv.lock or
# pyproject.toml change. --frozen because uv cannot validate the lockfile
# without workspace member sources; the final sync below runs with --locked.
RUN --mount=type=cache,target=/home/apiuser/.cache/uv,uid=1001,gid=1001 \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-workspace --no-dev --group admin --no-editable
COPY --chown=apiuser:apiuser ./tracecat /app/tracecat
COPY --chown=apiuser:apiuser ./packages /app/packages
COPY --from=plugin-skills --chown=apiuser:apiuser /skills/ /var/lib/tracecat/copilot-skills/
COPY --chown=apiuser:apiuser ./pyproject.toml ./uv.lock ./.python-version ./README.md ./LICENSE ./alembic.ini /app/
COPY --chown=apiuser:apiuser ./alembic /app/alembic
RUN --mount=type=cache,target=/home/apiuser/.cache/uv,uid=1001,gid=1001 \
uv sync --locked --no-dev --group admin --no-editable
ENV PATH="/app/.venv/bin:/home/apiuser/.local/bin:/usr/local/bin:/usr/bin:/bin"
RUN ln -sf $(which uv) /home/apiuser/.local/bin/uv
# ====================
# Stage 11: Production registry manifest
# ====================
FROM production-app AS registry-manifest
RUN /app/.venv/bin/python -m tracecat.registry.sync.prebuild
# ====================
# Stage 12: Production target
# ====================
FROM production-app AS production
# Carry only the generated builtin registry metadata in the image so platform
# registry startup can update the DB without rediscovering actions on the hot path.
COPY --from=registry-manifest --chown=apiuser:apiuser /app/.registry-artifacts /app/.registry-artifacts
# Verification
RUN nsjail --help > /dev/null 2>&1 && echo "nsjail available"
# Build metadata shown to platform admins. Declared last so a new commit only
# rebuilds this metadata layer.
ARG TRACECAT__BUILD_COMMIT
ARG TRACECAT__BUILD_TIME
ENV TRACECAT__BUILD_COMMIT=${TRACECAT__BUILD_COMMIT} \
TRACECAT__BUILD_TIME=${TRACECAT__BUILD_TIME}
EXPOSE $PORT
CMD ["sh", "-c", "python3 -m uvicorn tracecat.api.app:app --host $HOST --port $PORT"]