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
29 changes: 27 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,41 @@ node_modules
# Next.js build output
.next
out
custom-server.js
*.tsbuildinfo

# Local test infrastructure (pnpm test:ui), including its own .next cache
.test

# Production folders (should be volume mounted, not baked into image)
backups
storage
data
db/*.db
db/*.db-journal
prisma/*.db
prisma/*.db-*

# Local test and canary data
.canary
.ssh-test
**/.ssh

# Certificates and private keys
**/*.key
**/*.pem
**/*.crt
**/*.cer
**/*.p12
**/*.pfx
**/*.ppk
**/id_rsa
**/id_dsa
**/id_ecdsa
**/id_ed25519

# Environment variables
.env*.local
.env
.env*

# Version control
.git
Expand All @@ -29,6 +51,9 @@ db/*.db-journal
Dockerfile
docker-compose*.yml
.dockerignore
.codex-docker-context
.docker-build-context*
.docker-context*

# IDEs
.vscode
Expand Down
190 changes: 156 additions & 34 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -132,50 +132,158 @@ RUN printf '#!/bin/sh\nexec /usr/share/dotnet/dotnet /opt/sqlpackage/sqlpackage.
chmod +x /usr/local/bin/sqlpackage && \
sqlpackage /version

# Enable corepack for pnpm support and symlink PostgreSQL 18 binaries
# Symlink PostgreSQL 18 binaries into PATH.
# On Debian with PGDG, pg binaries live under /usr/lib/postgresql/18/bin/
RUN corepack enable && corepack prepare pnpm@10.29.3 --activate && \
ln -sf /usr/lib/postgresql/18/bin/pg_dump /usr/local/bin/pg_dump && \
RUN ln -sf /usr/lib/postgresql/18/bin/pg_dump /usr/local/bin/pg_dump && \
ln -sf /usr/lib/postgresql/18/bin/pg_restore /usr/local/bin/pg_restore && \
ln -sf /usr/lib/postgresql/18/bin/psql /usr/local/bin/psql

# Validate pg_dump version resolves correctly (fail-fast on broken symlinks/packages)
RUN pg_dump --version | grep -q 'PostgreSQL) 18\.' || \
(echo "ERROR: pg_dump version validation failed! Check PostgreSQL 18 client package." && exit 1)

# 1. Install Dependencies
FROM base AS deps
# Keep pnpm and its cross-platform Corepack cache out of the runtime image.
FROM base AS target-build-base
RUN corepack enable && corepack prepare pnpm@10.29.3 --activate

# Install target-platform dependencies only for native runtime payloads.
FROM target-build-base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm-${TARGETARCH},target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile

# Install only the lock-resolved Prisma CLI and its runtime dependencies.
FROM deps AS prisma-cli
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN --mount=type=cache,id=pnpm-${TARGETARCH},target=/root/.local/share/pnpm/store \
PRISMA_VERSION="$(node -p 'require("/app/node_modules/prisma/package.json").version')" && \
pnpm add --global "prisma@${PRISMA_VERSION}" && \
prisma --version

# Generate the target-platform Prisma client and Sharp native packages. The full
# dependency tree remains in this disposable stage and never reaches the image.
FROM deps AS target-native
COPY prisma/schema.prisma ./prisma/schema.prisma
RUN pnpm prisma generate && \
PRISMA_CLIENT="$(node -e 'const path = require("node:path"); process.stdout.write(path.resolve(path.dirname(require.resolve("@prisma/client/package.json")), "../../.prisma/client"))')" && \
test -d "$PRISMA_CLIENT" && \
mkdir -p /target-native && \
cp -aL "$PRISMA_CLIENT" /target-native/prisma-client && \
SHARP_ARCH="$(node -p 'process.arch')" && \
copy_target_sharp_package() { \
PACKAGE="$1"; \
PACKAGE_METADATA="$2"; \
SOURCE_PACKAGE="$(node -e 'const { createRequire } = require("node:module"); const sharpRequire = createRequire(require.resolve("sharp")); process.stdout.write(sharpRequire.resolve(process.argv[1]))' "$PACKAGE_METADATA")" || return 1; \
test -s "$SOURCE_PACKAGE" || return 1; \
DESTINATION="/target-native/node_modules/$PACKAGE"; \
mkdir -p "$(dirname "$DESTINATION")" && \
cp -aL "$(dirname "$SOURCE_PACKAGE")" "$DESTINATION"; \
}; \
copy_target_sharp_package "@img/sharp-linux-$SHARP_ARCH" "@img/sharp-linux-$SHARP_ARCH/package" && \
copy_target_sharp_package "@img/sharp-libvips-linux-$SHARP_ARCH" "@img/sharp-libvips-linux-$SHARP_ARCH/package"

# Build the CPU-neutral application once on the native Linux build platform.
FROM --platform=$BUILDPLATFORM node:24-slim AS app-build-base
ARG BUILDARCH
RUN apt-get update && apt-get install -y --no-install-recommends openssl util-linux && \
rm -rf /var/lib/apt/lists/* && \
corepack enable && \
corepack prepare pnpm@10.29.3 --activate

FROM app-build-base AS app-deps
ARG BUILDARCH
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
RUN --mount=type=cache,id=pnpm-${BUILDARCH},target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile

# 2. Builder Phase
FROM base AS builder
FROM app-build-base AS builder
ARG BUILDARCH
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=app-deps /app/node_modules ./node_modules
COPY . .

# Environment variables for build
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_OPTIONS="--max-old-space-size=4096"
ENV DBACKUP_DOCKER_BUILD=1
ENV RAYON_NUM_THREADS=1
ENV TOKIO_WORKER_THREADS=1
ENV NEXT_WEBPACK_PARALLELISM=1
ENV NODE_OPTIONS="--max-old-space-size=1792"

# Generate Prisma Client and build the Next.js app.
# The Docker-only Next.js config externalizes large Node-only dependency graphs
# and delegates type-checking to fresh TypeScript processes below.
RUN --mount=type=cache,id=next-build-${BUILDARCH},target=/app/.next/cache \
pnpm prisma generate && \
BUILD_CPU="$(awk '$1 == "Cpus_allowed_list:" { split($2, groups, ","); split(groups[1], range, "-"); print range[1] }' /proc/self/status)" && \
test -n "$BUILD_CPU" && \
taskset --cpu-list "$BUILD_CPU" pnpm exec next build --webpack

# Sharp is a direct runtime dependency, but Next's explicit Sharp trace can omit
# pnpm-hoisted transitive packages. Copy only Sharp's target-native runtime closure.
RUN SHARP_ARCH="$(node -p 'process.arch')" && \
copy_sharp_package() { \
PACKAGE="$1"; \
PACKAGE_METADATA="$2"; \
SOURCE_PACKAGE="$(node -e 'const { createRequire } = require("node:module"); const sharpRequire = createRequire(require.resolve("sharp")); process.stdout.write(sharpRequire.resolve(process.argv[1]))' "$PACKAGE_METADATA")" || return 1; \
test -s "$SOURCE_PACKAGE" || return 1; \
DESTINATION=".next/standalone/node_modules/$PACKAGE"; \
mkdir -p "$(dirname "$DESTINATION")" && \
rm -rf "$DESTINATION" && \
cp -aL "$(dirname "$SOURCE_PACKAGE")" "$DESTINATION"; \
}; \
copy_sharp_package detect-libc detect-libc/package.json && \
copy_sharp_package semver semver/package.json && \
copy_sharp_package @img/colour @img/colour/package.json && \
copy_sharp_package "@img/sharp-linux-$SHARP_ARCH" "@img/sharp-linux-$SHARP_ARCH/package" && \
copy_sharp_package "@img/sharp-libvips-linux-$SHARP_ARCH" "@img/sharp-libvips-linux-$SHARP_ARCH/package"

# Compile-check the application and custom server in separate cacheable processes.
RUN \
NODE_OPTIONS="--max-old-space-size=2560" pnpm exec tsc --noEmit --incremental false && \
NODE_OPTIONS="--max-old-space-size=2560" pnpm exec tsc -p tsconfig.server.json --incremental false

# Generate Prisma Client, build Next.js app, and compile custom server
# --mount=type=cache persists the Next.js incremental build cache (.next/cache)
# across Docker builds via GitHub Actions cache (type=gha,mode=max in release.yml).
# Next.js reuses webpack/SWC artefacts for unchanged modules, cutting rebuild time significantly.
RUN --mount=type=cache,id=next-cache,target=/app/.next/cache \
pnpm prisma generate && pnpm run build && npx tsc -p tsconfig.server.json
# Verify that standalone output is complete and contains only Linux native addons.
RUN test -s .next/standalone/server.js && \
test -s .next/standalone/.next/required-server-files.json && \
test -s .next/standalone/.next/BUILD_ID && \
test -d .next/standalone/node_modules/next && \
test -s custom-server.js && \
node --check custom-server.js && \
test -n "$(find .next/standalone -type f -name 'libquery_engine-*.so.node' -print -quit)" && \
test -n "$(find .next/standalone -type f -path '*sharp-linux-*' -name '*.node' -print -quit)" && \
! grep -R -E --include='*.nft.json' 'query_engine-windows|sharp-win32' .next && \
node -e 'const { createRequire } = require("node:module"); const runtimeRequire = createRequire("/app/.next/standalone/server.js"); for (const dependency of ["next", "@prisma/client", "sharp"]) { const resolved = runtimeRequire.resolve(dependency); if (!resolved.startsWith("/app/.next/standalone/")) throw new Error(`${dependency} resolved outside standalone output: ${resolved}`); runtimeRequire(dependency) }' && \
find .next/standalone -type f -name '*.node' -exec sh -ec 'for file do signature=$(od -An -tx1 -N4 "$file" | tr -d " \n"); test "$signature" = 7f454c46 || { echo "Non-ELF native addon: $file ($signature)" >&2; exit 1; }; done' sh {} + && \
BAD_NATIVE="$(find .next/standalone -type f \( -name '*windows*.node' -o -name '*.dll.node' -o -path '*/sharp-win32-*/*' \) -print -quit)" && \
test -z "$BAD_NATIVE"

# Remove build-platform native payloads before the runtime stage copies the
# standalone tree. Target-native Prisma and Sharp files are grafted below.
FROM builder AS portable-builder
RUN PRISMA_CLIENT="$(node -e 'const path = require("node:path"); const { createRequire } = require("node:module"); const runtimeRequire = createRequire("/app/.next/standalone/server.js"); process.stdout.write(path.resolve(path.dirname(runtimeRequire.resolve("@prisma/client/package.json")), "../../.prisma/client"))')" && \
test -d "$PRISMA_CLIENT" && \
rm -rf "$PRISMA_CLIENT" && \
rm -rf \
.next/standalone/node_modules/@img/sharp-linux-* \
.next/standalone/node_modules/@img/sharp-libvips-linux-* \
.next/standalone/node_modules/.pnpm/@img+sharp-linux-* \
.next/standalone/node_modules/.pnpm/@img+sharp-libvips-linux-* && \
test -z "$(find .next/standalone -type f -name '*.node' -print -quit)"

# 3. Runner Phase (The actual image)
FROM base AS runner
ARG TARGETARCH
WORKDIR /app

# The Recovery Kit reads this off disk when a user downloads one, so it has to be in the
# image. A missing file is not a build error - the kit is generated with a placeholder
# apologising for its absence, which nobody discovers until they need it. Guarded by
# tests/unit/lint-guards/recovery-kit-shipped.test.ts.
COPY --from=builder --link --chown=1001:1001 /app/scripts/dbackup-recover.js ./scripts/dbackup-recover.js
COPY --from=portable-builder --link --chown=1001:1001 /app/scripts/dbackup-recover.js ./scripts/dbackup-recover.js

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
Expand All @@ -193,29 +301,43 @@ RUN groupadd --system --gid 1001 nodejs && \
useradd --system --uid 1001 --gid nodejs --no-create-home nextjs

# Copy built files (--link for better layer caching)
COPY --from=builder --link --chown=1001:1001 /app/public ./public
COPY --from=builder --link --chown=1001:1001 /app/.next/standalone ./
COPY --from=builder --link --chown=1001:1001 /app/.next/static ./.next/static
COPY --from=builder --link --chown=1001:1001 /app/prisma ./prisma

# Create runtime data directory + install Prisma CLI for migrations
# Note: pnpm add -g runs as root, so we must chown /pnpm to the runtime user
# to avoid "Can't write to @prisma/engines" errors at container startup
# Prisma version is read from package.json to stay in sync automatically
COPY --from=builder --link /app/package.json /tmp/package.json
COPY --from=portable-builder --link --chown=1001:1001 /app/public ./public
COPY --from=portable-builder --link --chown=1001:1001 /app/.next/standalone ./
COPY --from=portable-builder --link --chown=1001:1001 /app/.next/static ./.next/static
COPY --from=portable-builder --link --chown=1001:1001 /app/prisma ./prisma

# Graft only the target-platform native runtime payload into the portable tree.
RUN --mount=type=bind,from=target-native,source=/target-native,target=/target-native,ro \
RUNTIME_CLIENT="$(node -e 'const path = require("node:path"); const { createRequire } = require("node:module"); const runtimeRequire = createRequire("/app/server.js"); process.stdout.write(path.resolve(path.dirname(runtimeRequire.resolve("@prisma/client/package.json")), "../../.prisma/client"))')" && \
mkdir -p "$RUNTIME_CLIENT" /app/node_modules/@img && \
cp -a /target-native/prisma-client/. "$RUNTIME_CLIENT"/ && \
cp -a /target-native/node_modules/@img/. /app/node_modules/@img/ && \
chown -R 1001:1001 "$RUNTIME_CLIENT" /app/node_modules/@img

# Create runtime data directories and copy the minimal Prisma CLI tree.
RUN mkdir -p /data/storage/avatars /data/db /data/certs && \
chown -R 1001:1001 /data && \
PRISMA_VERSION=$(node -e "console.log(require('/tmp/package.json').devDependencies.prisma.replace(/[\^~>=<]/g,''))") && \
pnpm add -g prisma@${PRISMA_VERSION} && \
rm /tmp/package.json && \
chown -R 1001:1001 /pnpm
chown -R 1001:1001 /data
COPY --from=prisma-cli --link --chown=1001:1001 /pnpm /pnpm

# Copy compiled custom HTTPS server (replaces default Next.js server entry point)
COPY --from=builder --link --chown=1001:1001 /app/custom-server.js ./custom-server.js
COPY --from=portable-builder --link --chown=1001:1001 /app/custom-server.js ./custom-server.js

# Fail the build if standalone tracing missed required native runtime packages.
RUN node --check custom-server.js && \
prisma --version && \
node -e 'Promise.all(["@aws-sdk/lib-storage", "@microsoft/microsoft-graph-client", "dockerode", "dropbox", "googleapis", "mssql", "ssh2", "ssh2-sftp-client"].map(async (dependency) => { try { await import(dependency); console.log(`${dependency}: available`) } catch (error) { console.error(`${dependency}: ${error.code ?? error.message}`); process.exitCode = 1 } }))' && \
node -e 'require("@prisma/client"); require("sharp")' && \
test -n "$(find /app/node_modules -type f -name 'libquery_engine-*.so.node' -print -quit)" && \
test -n "$(find /app/node_modules -type f -path '*sharp-linux-*' -name '*.node' -print -quit)" && \
EXPECTED_MACHINE="$(case "$TARGETARCH" in amd64) echo 3e00 ;; arm64) echo b700 ;; *) exit 1 ;; esac)" && \
find / -xdev -type f -name '*.node' -exec sh -ec 'expected="$1"; shift; for file do signature=$(od -An -tx1 -N4 "$file" | tr -d " \n"); machine=$(od -An -tx1 -j18 -N2 "$file" | tr -d " \n"); test "$signature" = 7f454c46 && test "$machine" = "$expected" || { echo "Wrong native addon: $file (signature=$signature machine=$machine expected=$expected)" >&2; exit 1; }; done' sh "$EXPECTED_MACHINE" {} + && \
BAD_NATIVE="$(find / -xdev -type f \( -name '*windows*.node' -o -name '*.dll.node' -o -path '*/sharp-win32-*/*' \) -print -quit)" && \
test -z "$BAD_NATIVE"

# Copy entrypoint script
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
RUN sed -i 's/\r$//' /usr/local/bin/docker-entrypoint.sh && \
chmod +x /usr/local/bin/docker-entrypoint.sh

# Health check: verify app + database are reachable
# Uses --insecure for self-signed certs; falls back to http if DISABLE_HTTPS=true
Expand Down
16 changes: 16 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

All notable changes to DBackup are documented here.

## vNEXT
*Release: In Progress*

### ✨ Features

- **MongoDB**: Backup jobs can now create and restore one native full instance archive containing all databases, users and custom roles. ([#152](https://github.com/Skyfay/DBackup/pull/152))

### 🐛 Bug Fixes

- **restore**: Metadata marked with no compression is now restored without entering decompression. ([#152](https://github.com/Skyfay/DBackup/pull/152))
- **restore**: Concurrent restores now use isolated temporary files so metadata and archives cannot overwrite each other. ([#152](https://github.com/Skyfay/DBackup/pull/152))

### 🔧 CI/CD

- **docker**: Docker images now use native Linux standalone output without copying the complete production dependency tree. ([#152](https://github.com/Skyfay/DBackup/pull/152))

## v3.3.0 - Azure SQL Database Support, S3 Upload Rework and General Improvements
*Released: Aug 15, 2026*

Expand Down
48 changes: 47 additions & 1 deletion docs/user-guide/sources/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ mongorestore
mongosh
```

::: info Full Instance tool version
Full Instance backup and restore require MongoDB Database Tools 100.3 or newer on the host that runs the tools. The DBackup Docker image already meets this requirement. In SSH mode, install a compatible version on the remote host.
:::

**Install on the remote host:**

<details>
Expand Down Expand Up @@ -168,6 +172,31 @@ For Atlas clusters, create a user with "Backup Admin" role in the Atlas UI.

## Backup Process

### Backup Scope

MongoDB jobs provide two backup scopes:

- **Selected Databases** is the default and preserves the existing behavior. Choose one or more databases in the job. Restore can select individual databases and map them to different target names.
- **Full Instance** runs one native `mongodump` archive for the server. It includes every database available to the backup user, plus MongoDB users and custom roles. The database selector is hidden because this scope cannot be narrowed to individual databases.

Existing jobs and backup files without scope metadata are always treated as **Selected Databases**.

Full Instance executes the equivalent of:

```bash
mongodump <connection arguments> --archive=<path> --gzip
```

MongoDB's normal `mongodump` rules still apply. For example, server-local replication data in the `local` database is not a portable part of an instance backup.

::: warning Permissions
Use a credential with the `backup` role on `admin`. Without sufficient access to the `admin` database, the archive may not contain the users and custom roles needed for a complete instance restore.
:::

::: warning Replica Set Consistency
Full Instance is a logical dump and currently does not add `--oplog`. On an active replica set, writes can continue while collections are being dumped, so the archive is not a point-in-time-consistent snapshot. Use a maintenance window or MongoDB's coordinated snapshot tooling when point-in-time consistency is required.
:::

### Direct Mode

DBackup uses `mongodump` which creates a binary BSON dump:
Expand Down Expand Up @@ -358,9 +387,26 @@ To restore a MongoDB backup:
2. Find your backup file
3. Click **Restore**
4. Select target database configuration
5. Optionally map database names
5. For a **Selected Databases** backup, optionally select and map database names
6. Confirm and monitor progress

For a **Full Instance** backup, DBackup hides database mapping and restores the complete native archive.

### Full Instance Restore Safety

A Full Instance restore is destructive. It runs `mongorestore` with `--drop` and replaces the target's MongoDB users and custom roles with the definitions from the backup. Authentication credentials on the target can therefore change during the restore.

The credential DBackup uses must continue to work after those users are replaced. The safest approach is to use a restore administrator that exists both on the target and in the backup with the same username, password, authentication database, and required restore permissions. Otherwise MongoDB can reject new connections part-way through the restore, leaving the target only partially restored.

Before starting:

1. Stop applications that write to the target.
2. Back up the target instance or use a disposable restore target.
3. Confirm that the DBackup target credential will remain valid after the source users and roles are restored.
4. Keep the target MongoDB version compatible with the backup version.

After the restore succeeds, verify application data, users, and custom roles before directing production traffic to the target.

### Restore Options

- **Drop existing data**: Clean restore
Expand Down
Loading