Dockerfile
Rama main · réplica de lectura del repositorio original.
# ── NineDeploy container image ───────────────────────────────────────────
# NOTE: the recommended production setup is bare-metal (install.sh + systemd)
# because the core needs direct Docker daemon + PM2 access. This image is for
# CI build verification and OPTIONAL containerized deploys — run it with the
# host Docker socket mounted so the deploy engine can manage sibling containers:
#
# docker run -d --name ninedeploy \
# -v /var/run/docker.sock:/var/run/docker.sock \
# --group-add $(getent group docker | cut -d: -f3) \
# -v ninedeploy-data:/data \
# -p 3000:3000 \
# -e NINEDEPLOY_DATA_DIR=/data \
# -e NINEDEPLOY_DB_PATH=/data/ninedeploy.db \
# -e NINEDEPLOY_JWT_SECRET=<strong-secret> \
# -e NINEDEPLOY_PUBLIC_URL=https://your-host \
# ghcr.io/ninedeploy/ninedeploy
#
# CONTAINERIZED TRAEFIK CAVEAT: the panel mounts its Traefik config directory
# (`${NINEDEPLOY_DATA_DIR}/traefik`) into the Traefik container as a
# HOST-PATH bind, so that path must resolve to the SAME files on the docker
# host. Bind the host's /data straight through at the SAME absolute path
# (`-v /data:/data`) instead of using a named volume, or Traefik reads an
# empty directory and every domain answers 404. The bare-metal install
# (install.sh) shares the host filesystem by construction and has no such
# requirement.
# The --group-add is REQUIRED when running as the non-root `ninedeploy` user:
# the host socket is typically root:docker mode 0660, and without the host
# docker group's GID every `docker` call fails with permission denied.
#
# PM2-based services are not available in this mode (the daemon runs in the
# host PID space); Docker-based services and templates work normally.
# The security-release image index can precede its platform manifests. Use
# the official, checksum-pinned Node binaries in a shared stage so neither
# cached base images nor registry propagation can leave an old runtime.
FROM node:26-slim AS secure-node
ARG TARGETARCH
ENV NODE_VERSION=26.12.1
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl xz-utils \
&& case "$TARGETARCH" in \
amd64) NODE_ARCH="x64"; NODE_SHA256="6170b2d474e1c626f43cff3adc118df9fe3b573258a4fc3d850c8dbc6b079bbd" ;; \
arm64) NODE_ARCH="arm64"; NODE_SHA256="42aa04922591ee5d4dbc7740d452621acf28508239daa512f5ee8c47ee224fdb" ;; \
*) echo "Unsupported Node architecture: $TARGETARCH" >&2; exit 1 ;; \
esac \
&& curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" -o /tmp/node-security.tar.xz \
&& echo "$NODE_SHA256 /tmp/node-security.tar.xz" | sha256sum -c - \
&& tar -xJf /tmp/node-security.tar.xz -C /usr/local --strip-components=1 \
&& test "$(node --version)" = "v${NODE_VERSION}" \
&& rm -f /tmp/node-security.tar.xz \
&& rm -rf /var/lib/apt/lists/*
# ── Stage 1: build the monorepo ──────────────────────────────────────────
FROM secure-node AS build
WORKDIR /app
# Native modules (ssh2 -> node-gyp) need a toolchain at install time;
# The slim base ships without python3/make/g++ and the install fails
# nondeterministically depending on which optional deps resolve.
RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/*
# Node ≥ 26 images no longer bundle corepack, so install pnpm via npm.
# Keep PNPM_VERSION in sync with "packageManager" in package.json.
ARG PNPM_VERSION=11.23.0
RUN npm install -g pnpm@${PNPM_VERSION}
# Copy workspace manifests first for layer-cached installs.
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .npmrc ./
COPY patches/ ./patches/
COPY apps/server/package.json apps/server/
COPY apps/web/package.json apps/web/
COPY apps/cli/package.json apps/cli/
COPY packages/db/package.json packages/db/
COPY packages/schemas/package.json packages/schemas/
COPY packages/sdk/package.json packages/sdk/
COPY packages/plugin-sdk/package.json packages/plugin-sdk/
COPY packages/mcp/package.json packages/mcp/
RUN pnpm install --frozen-lockfile
# Copy the rest and build (tsc for server/packages, vite for web).
COPY . .
RUN node scripts/check-node-version.mjs && pnpm build
# ── Stage 2: runtime ─────────────────────────────────────────────────────
FROM secure-node AS runtime
WORKDIR /app
ARG NIXPACKS_VERSION=1.41.0
ARG TARGETARCH
# docker CLI: the deploy engine shells out to `docker` (via the mounted socket).
# Debian trixie split the CLI out of docker.io into its own docker-cli package —
# docker.io alone no longer provides /usr/bin/docker.
# git: repo checkouts. tini: proper signal handling / zombie reaping as PID 1.
# openssh-client (D3): deploy-key clones run `ssh -i <key>` (lib/git.ts, and on
# a node agent `git.withKey`), and with --no-install-recommends git does NOT
# pull it in (ssh-client is only a Recommends of Debian's git), so every
# deploy-key clone in a docker install — and on every node agent, which runs
# this image — failed with "cannot run ssh".
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl docker-cli git openssh-client tini \
&& case "$TARGETARCH" in \
amd64) NIXPACKS_TARGET="x86_64-unknown-linux-musl"; NIXPACKS_SHA256="0f55de7874507b9cf7502113120bd96f2ab6979f78d10eaf2eb2ade9207b3af6" ;; \
arm64) NIXPACKS_TARGET="aarch64-unknown-linux-musl"; NIXPACKS_SHA256="912bd02dd2bb6f9c3a9ed965fe8a68b4aa318dc7a2546e2eca6f2806a894ba39" ;; \
*) echo "Unsupported Nixpacks architecture: $TARGETARCH" >&2; exit 1 ;; \
esac \
&& NIXPACKS_ASSET="nixpacks-v${NIXPACKS_VERSION}-${NIXPACKS_TARGET}.tar.gz" \
&& curl -fsSL "https://github.com/railwayapp/nixpacks/releases/download/v${NIXPACKS_VERSION}/${NIXPACKS_ASSET}" -o "/tmp/${NIXPACKS_ASSET}" \
&& echo "${NIXPACKS_SHA256} /tmp/${NIXPACKS_ASSET}" | sha256sum -c - \
&& tar -xzf "/tmp/${NIXPACKS_ASSET}" -C /usr/local/bin nixpacks \
&& chmod 0755 /usr/local/bin/nixpacks \
&& nixpacks --version \
&& rm -f "/tmp/${NIXPACKS_ASSET}" \
&& rm -rf /var/lib/apt/lists/*
# Docker CLI plugins. Debian's `docker.io` ships the bare CLI only, so every
# compose service (`docker compose up`) and every BuildKit build
# (`docker buildx build`, engine:use_buildkit) failed inside this image with
# "'compose' is not a docker command". Pinned + checksum-verified like Nixpacks.
ARG COMPOSE_VERSION=5.5.1
ARG BUILDX_VERSION=0.37.1
RUN case "$TARGETARCH" in \
amd64) COMPOSE_ARCH="x86_64"; COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"; \
BUILDX_SHA256="9447199cdb435f25880548343c128a4b6650e8891ee598905d8d29d39a8e359b" ;; \
arm64) COMPOSE_ARCH="aarch64"; COMPOSE_SHA256="732e3a84c1a0f67256ce80bc2598a24546b10ca05f9faa97efceb1171ece2ef7"; \
BUILDX_SHA256="e5cc9fe3bbff5cbc91230981f7860e06076110730a2db997082652199042a1f2" ;; \
*) echo "Unsupported Docker plugin architecture: $TARGETARCH" >&2; exit 1 ;; \
esac \
&& PLUGINS=/usr/local/lib/docker/cli-plugins \
&& mkdir -p "$PLUGINS" \
&& curl -fsSL "https://github.com/docker/compose/releases/download/v${COMPOSE_VERSION}/docker-compose-linux-${COMPOSE_ARCH}" -o "$PLUGINS/docker-compose" \
&& echo "${COMPOSE_SHA256} $PLUGINS/docker-compose" | sha256sum -c - \
&& curl -fsSL "https://github.com/docker/buildx/releases/download/v${BUILDX_VERSION}/buildx-v${BUILDX_VERSION}.linux-${TARGETARCH}" -o "$PLUGINS/docker-buildx" \
&& echo "${BUILDX_SHA256} $PLUGINS/docker-buildx" | sha256sum -c - \
&& chmod 0755 "$PLUGINS/docker-compose" "$PLUGINS/docker-buildx" \
&& docker compose version \
&& docker buildx version
# r582: Railpack CLI, the optional `railpack` build pack. Same version and
# SHA-256 pins as install.sh (bare metal) — keep the two in sync. Without it
# a container install could only refuse the build pack (r520). Railpack builds
# through its own BuildKit client, so it also needs BUILDKIT_HOST pointing at a
# BuildKit daemon (e.g. `-e BUILDKIT_HOST=docker-container://buildkit` next to
# a `moby/buildkit` container); the panel refuses the pack with that fix named
# until it is set.
ARG RAILPACK_VERSION=0.39.0
RUN case "$TARGETARCH" in \
amd64) RAILPACK_TARGET="x86_64-unknown-linux-musl"; RAILPACK_SHA256="728407f5cdb9e9bc1cdd07f568419344a20e71b0a5a9fd90a9cfbaca0a6c94f7" ;; \
arm64) RAILPACK_TARGET="arm64-unknown-linux-musl"; RAILPACK_SHA256="42eb3fa68e38f44be3610a7d74f714ec0d808d70c105fbe35e053b6e6cfb20be" ;; \
*) echo "Unsupported Railpack architecture: $TARGETARCH" >&2; exit 1 ;; \
esac \
&& RAILPACK_ASSET="railpack-v${RAILPACK_VERSION}-${RAILPACK_TARGET}.tar.gz" \
&& curl -fsSL "https://github.com/railwayapp/railpack/releases/download/v${RAILPACK_VERSION}/${RAILPACK_ASSET}" -o "/tmp/${RAILPACK_ASSET}" \
&& echo "${RAILPACK_SHA256} /tmp/${RAILPACK_ASSET}" | sha256sum -c - \
&& tar -xzf "/tmp/${RAILPACK_ASSET}" -C /usr/local/bin railpack \
&& chmod 0755 /usr/local/bin/railpack \
&& railpack --version \
&& rm -f "/tmp/${RAILPACK_ASSET}"
# G4.4: the pack CLI (Cloud Native Buildpacks), for the optional `paketo` and
# `heroku` build packs. Same version and SHA-256 pins as install.sh — keep the
# two in sync. pack drives the host's Docker daemon through the mounted socket
# and copies the source in itself, so no path mapping or BuildKit is needed.
ARG PACK_VERSION=0.40.9
RUN case "$TARGETARCH" in \
amd64) PACK_ASSET="pack-v${PACK_VERSION}-linux.tgz"; PACK_SHA256="dc0ee1e931cf8a106d7555a01a214864f9acb60b77adf15d69b74df4404758e9" ;; \
arm64) PACK_ASSET="pack-v${PACK_VERSION}-linux-arm64.tgz"; PACK_SHA256="091ccb213823656c727731537ef8f1000eb4dc3ec61641506653e7f9d6da0c5e" ;; \
*) echo "Unsupported pack architecture: $TARGETARCH" >&2; exit 1 ;; \
esac \
&& curl -fsSL "https://github.com/buildpacks/pack/releases/download/v${PACK_VERSION}/${PACK_ASSET}" -o "/tmp/${PACK_ASSET}" \
&& echo "${PACK_SHA256} /tmp/${PACK_ASSET}" | sha256sum -c - \
&& tar -xzf "/tmp/${PACK_ASSET}" -C /usr/local/bin pack \
&& chmod 0755 /usr/local/bin/pack \
&& pack version \
&& rm -f "/tmp/${PACK_ASSET}"
# Node ≥ 26 images no longer bundle corepack — see stage 1. Keep in sync with
# "packageManager" in package.json.
ARG PNPM_VERSION=11.23.0
RUN npm install -g pnpm@${PNPM_VERSION}
# Production dependencies only (dev deps are stripped). apps/web's manifest is
# copied just to satisfy the workspace resolution — its deps are never installed
# into the runtime (--filter keeps it to what apps/server imports).
COPY --from=build /app/package.json /app/pnpm-workspace.yaml /app/pnpm-lock.yaml /app/.npmrc ./
COPY --from=build /app/patches/ patches/
COPY --from=build /app/apps/server/package.json apps/server/
COPY --from=build /app/apps/web/package.json apps/web/
COPY --from=build /app/apps/cli/package.json apps/cli/
COPY --from=build /app/packages/db/package.json packages/db/
COPY --from=build /app/packages/schemas/package.json packages/schemas/
COPY --from=build /app/packages/sdk/package.json packages/sdk/
COPY --from=build /app/packages/plugin-sdk/package.json packages/plugin-sdk/
COPY --from=build /app/packages/mcp/package.json packages/mcp/
RUN pnpm install --frozen-lockfile --prod --filter @ninedeploy/server...
# Compiled output (the templates registry compiles into dist; the web dashboard
# is served by the API itself via @fastify/static with an SPA fallback).
COPY --from=build /app/apps/server/dist apps/server/dist
COPY --from=build /app/apps/web/dist apps/web/dist
COPY --from=build /app/packages/db/dist packages/db/dist
# SQL migrations: the server self-migrates at startup via the runtime migrator
# (drizzle-kit is a devDependency and absent here).
COPY --from=build /app/packages/db/src/migrations packages/db/src/migrations
COPY --from=build /app/packages/schemas/dist packages/schemas/dist
COPY --from=build /app/packages/sdk/dist packages/sdk/dist
COPY --from=build /app/packages/plugin-sdk/dist packages/plugin-sdk/dist
COPY --from=build /app/packages/mcp/dist packages/mcp/dist
# Non-root user; /data is the volume mount point for db/repos/logs/backups.
RUN useradd --system --create-home ninedeploy && mkdir -p /data && chown ninedeploy /data
USER ninedeploy
ENV NODE_ENV=production \
NINEDEPLOY_HOST=0.0.0.0 \
NINEDEPLOY_PORT=3000 \
NINEDEPLOY_DATA_DIR=/data \
NINEDEPLOY_DB_PATH=/data/ninedeploy.db
EXPOSE 3000
VOLUME /data
# The panel's /health includes DB readiness. The same image runs node agents,
# whose public /agent/ping confirms the HTTP listener without credentials.
# Probe the role's configured internal port, independently of published ports.
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD ["node", "-e", "const agent=process.env.NINEDEPLOY_AGENT==='1';const port=Number(agent?(process.env.NINEDEPLOY_AGENT_PORT||4600):(process.env.NINEDEPLOY_PORT||3000));fetch('http://127.0.0.1:'+port+(agent?'/agent/ping':'/health'),{signal:AbortSignal.timeout(3000)}).then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["node", "apps/server/dist/server.js"]