diff --git a/docker/Dockerfile.android b/docker/Dockerfile.android
new file mode 100644
index 0000000..1a57a81
--- /dev/null
+++ b/docker/Dockerfile.android
@@ -0,0 +1,39 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0
+
+# ── Java (required by the Android SDK tools) ─────────────────────────────────
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ openjdk-17-jdk-headless \
+ wget \
+ unzip \
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
+
+ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
+ENV ANDROID_HOME=/opt/android-sdk
+# .NET 10 android workload compiles against API level 36 (see error XA5207
+# if this drifts: the workload's Xamarin.Android.Tooling.targets dictates it).
+ENV ANDROID_SDK_PLATFORM=36
+ENV ANDROID_SDK_BUILD_TOOLS=36.0.0
+
+# ── Android command-line tools ────────────────────────────────────────────────
+# Pin the build number to a known-good release; update when Google breaks the URL.
+# Latest as of 2025: commandlinetools-linux-11076708_latest.zip (cmdline-tools 12.0)
+RUN mkdir -p "${ANDROID_HOME}/cmdline-tools" && \
+ wget -q "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \
+ -O /tmp/cmdtools.zip && \
+ unzip -q /tmp/cmdtools.zip -d "${ANDROID_HOME}/cmdline-tools" && \
+ mv "${ANDROID_HOME}/cmdline-tools/cmdline-tools" "${ANDROID_HOME}/cmdline-tools/latest" && \
+ rm /tmp/cmdtools.zip
+
+ENV PATH="${JAVA_HOME}/bin:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}"
+
+# ── Android SDK packages ──────────────────────────────────────────────────────
+RUN yes | sdkmanager --licenses > /dev/null && \
+ sdkmanager \
+ "platform-tools" \
+ "platforms;android-${ANDROID_SDK_PLATFORM}" \
+ "build-tools;${ANDROID_SDK_BUILD_TOOLS}"
+
+# ── dotnet android workload (~1 GB, baked into this layer) ───────────────────
+RUN dotnet workload install android
+
+WORKDIR /tmp/build
diff --git a/docker/Dockerfile.desktop b/docker/Dockerfile.desktop
new file mode 100644
index 0000000..a248159
--- /dev/null
+++ b/docker/Dockerfile.desktop
@@ -0,0 +1,11 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0
+
+# clang and zlib are required by the .NET SDK for native compilation steps
+# that occur even during managed cross-publish (win-x64 / linux-x64).
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ clang \
+ zlib1g-dev \
+ libkrb5-dev \
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /tmp/build
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 0000000..4190f92
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,183 @@
+# PalladiumWallet — Reproducible Builds via Docker
+
+This folder builds all three distribution targets — **Windows**, **Linux**,
+**Android** — inside Docker containers. The entire toolchain (.NET 10 SDK,
+JDK, Android SDK, android workload) lives in the container images, pinned in
+the Dockerfiles, so:
+
+- **anyone can produce the official binaries** with a single command, on any
+ Linux machine, without installing any SDK on the host;
+- **the toolchain never drifts**: every build uses exactly the same SDK
+ versions, regardless of what is installed (or updated) on the host;
+- **the build environment is auditable**: the Dockerfiles in this folder are
+ the complete, reviewable definition of how release binaries are made — which
+ matters for a wallet, where users must be able to trust that the shipped
+ binary comes from the published source.
+
+> **Scope note:** this pins the *build environment*. Bit-for-bit identical
+> output across machines is a stronger property that .NET does not guarantee
+> by default (embedded timestamps, signing); if two builds of the same commit
+> differ, they differ only in those metadata, not in code.
+
+---
+
+## Prerequisites
+
+A Linux host (native, WSL2, or a VM) with **Docker Engine** installed and
+running. Nothing else — no .NET SDK, no JDK, no Android SDK.
+
+```bash
+# Check that Docker works:
+docker info
+```
+
+If Docker is missing, install it from
+(or `sudo apt install docker.io` on Debian/Ubuntu). If `docker info` fails
+with a permission error, add yourself to the `docker` group and start a new
+shell:
+
+```bash
+sudo usermod -aG docker $USER
+```
+
+**Disk space:** ~2 GB for the desktop image, ~7 GB for the Android image
+(SDK + emulator-less toolchain). **First-run time:** the images are built
+automatically on first use — a few minutes for desktop, 10–20 minutes for
+Android (large downloads). Subsequent builds reuse the cached images and take
+well under a minute (desktop) / a few minutes (Android).
+
+---
+
+## Quick start
+
+```bash
+# From the repository root (or from the docker/ folder — both work):
+./docker/build.sh
+```
+
+Running without arguments shows an interactive menu — pick a single target or
+`all`. Non-interactive usage:
+
+```
+./docker/build.sh [TARGET] [--rebuild]
+
+Targets:
+ windows Win x64 single-file executable (native libs embedded)
+ linux Linux x64 single-file binary (runs as-is, nothing to install)
+ android Android APK (debug-signed)
+ all All three targets
+
+Options:
+ --rebuild Force rebuild of the Docker images (needed after editing a Dockerfile)
+```
+
+Examples:
+
+```bash
+./docker/build.sh all # build everything
+./docker/build.sh windows # Windows only
+./docker/build.sh android --rebuild # Android, rebuilding the image first
+```
+
+---
+
+## Output — and how to use each artifact
+
+All artifacts land in `dist/` at the repository root. The version number is
+read automatically from `` in `src/App/PalladiumWallet.App.csproj`.
+
+| Target | Path |
+|---------|--------------------------------------------------|
+| Windows | `dist/windows/PalladiumWallet-{ver}-win-x64.exe` |
+| Linux | `dist/linux/PalladiumWallet-{ver}-linux-x64` |
+| Android | `dist/android/PalladiumWallet-{ver}.apk` |
+
+**Windows** — a single self-contained `.exe` (runtime and native libraries
+embedded). Copy it to any 64-bit Windows 10/11 machine and double-click.
+The first launch takes a few extra seconds (it unpacks native libraries to a
+per-user cache); later launches are normal. SmartScreen may warn because the
+binary is not code-signed — choose "Run anyway".
+
+**Linux** — a single self-contained binary, already executable. Copy and run:
+
+```bash
+./PalladiumWallet-{ver}-linux-x64
+```
+
+Works on any desktop distro with glibc, X11/Wayland and fontconfig (i.e.
+effectively all of them); no .NET or other packages to install. If you
+transfer it through a channel that strips permissions (e.g. a web download),
+restore the execute bit with `chmod +x`.
+
+**Android** — a debug-signed APK for sideloading: transfer it to the phone
+and open it (enable "install from unknown sources" if prompted), or install
+via `adb install dist/android/PalladiumWallet-*.apk`. Supports Android 6.0+
+(API 23), arm64 phones and x86_64 emulators.
+
+> **Signature caveat:** debug-signed APKs built on different machines carry
+> different keys. If a previous build is already installed, Android refuses
+> the update (`INSTALL_FAILED_UPDATE_INCOMPATIBLE`) — uninstall the old app
+> first. **Uninstalling deletes the app's data: back up the wallet seed
+> before doing this.** Release signing with a stable key is not set up yet.
+
+---
+
+## How it works
+
+### Docker images
+
+| Image | Dockerfile | Used for | Size |
+|---------------------|----------------------|-----------------|---------|
+| `plm-build-desktop` | `Dockerfile.desktop` | windows + linux | ~1.5 GB |
+| `plm-build-android` | `Dockerfile.android` | android | ~5 GB |
+
+Images are built automatically the first time a target needs them and reused
+afterwards. Use `--rebuild` only after modifying a Dockerfile.
+
+### Source isolation
+
+The repository is mounted **read-only** inside the container; the build works
+on a copy at `/tmp/build`. Your working tree is never touched — no stray
+`bin/`/`obj/` directories, and a dirty working tree doesn't leak into the
+build beyond the files it contains. Artifacts are written back through a
+bind mount to `dist/` and chown'd to your user.
+
+### NuGet cache
+
+A Docker named volume `plm-nuget-cache` holds downloaded NuGet packages
+across builds. To reclaim the space or force a clean re-download:
+
+```bash
+docker volume rm plm-nuget-cache
+```
+
+---
+
+## Troubleshooting
+
+- **`Docker daemon is not running`** — start it (`sudo systemctl start
+ docker`; on WSL2, start Docker Desktop or the docker service).
+- **Android image build fails downloading `commandlinetools`** — Google
+ rotates the build number in the URL. Update the URL in
+ `Dockerfile.android` to the current one from
+ and rerun with
+ `--rebuild`.
+- **`error XA5207: Could not find android.jar for API level N`** — the .NET
+ android workload moved to a newer API level. Bump
+ `ANDROID_SDK_PLATFORM` (and `ANDROID_SDK_BUILD_TOOLS`) in
+ `Dockerfile.android` to the level the error names, then `--rebuild`.
+- **APK won't install over an existing app** — signature mismatch between
+ debug keys; see the signature caveat above.
+- **Everything is broken / start from scratch** —
+ `docker system prune -a && docker volume rm plm-nuget-cache`, then rerun
+ the script (images and packages are re-downloaded).
+
+---
+
+## Linux AppImage (future)
+
+The Linux target currently produces a single-file self-contained binary.
+Once a `pupnet.conf` is added to the repository, the `build_linux` function
+in `build.sh` can be extended to call PupNet Deploy inside the same
+`plm-build-desktop` image to also produce an AppImage with desktop
+integration (icon, menu entry).
diff --git a/docker/build.sh b/docker/build.sh
new file mode 100755
index 0000000..bd51c0d
--- /dev/null
+++ b/docker/build.sh
@@ -0,0 +1,201 @@
+#!/usr/bin/env bash
+# Reproducible build script for PalladiumWallet using Docker.
+# Run from anywhere; paths are resolved relative to this script.
+set -euo pipefail
+
+# ── Paths ─────────────────────────────────────────────────────────────────────
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
+DIST_DIR="${PROJECT_ROOT}/dist"
+
+# ── Docker image names ────────────────────────────────────────────────────────
+IMAGE_DESKTOP="plm-build-desktop"
+IMAGE_ANDROID="plm-build-android"
+
+# Named volume for NuGet package cache (shared across all builds, survives reboots).
+NUGET_VOLUME="plm-nuget-cache"
+
+# ── Helpers ───────────────────────────────────────────────────────────────────
+bold() { printf '\033[1m%s\033[0m\n' "$*"; }
+info() { printf '\033[1;34m>>>\033[0m %s\n' "$*"; }
+ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; }
+err() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; }
+die() { err "$*"; exit 1; }
+
+usage() {
+ cat </dev/null || die "Docker is not installed or not in PATH."
+docker info &>/dev/null || die "Docker daemon is not running."
+
+# ── Read project version from csproj ─────────────────────────────────────────
+CSPROJ="${PROJECT_ROOT}/src/App/PalladiumWallet.App.csproj"
+[[ -f "$CSPROJ" ]] || die "Cannot find $CSPROJ — run this script from the repo."
+VERSION=$(grep -oP '(?<=)[^<]+' "$CSPROJ") || die "Cannot extract from $CSPROJ."
+info "Version: ${VERSION}"
+
+# ── Ensure NuGet volume exists ────────────────────────────────────────────────
+docker volume inspect "$NUGET_VOLUME" &>/dev/null || \
+ docker volume create "$NUGET_VOLUME" > /dev/null
+
+# ── Image builders ────────────────────────────────────────────────────────────
+ensure_desktop_image() {
+ if [[ "$REBUILD" == true ]] || ! docker image inspect "$IMAGE_DESKTOP" &>/dev/null; then
+ info "Building Docker image: ${IMAGE_DESKTOP}"
+ docker build \
+ --tag "$IMAGE_DESKTOP" \
+ --file "${SCRIPT_DIR}/Dockerfile.desktop" \
+ "${SCRIPT_DIR}"
+ fi
+}
+
+ensure_android_image() {
+ if [[ "$REBUILD" == true ]] || ! docker image inspect "$IMAGE_ANDROID" &>/dev/null; then
+ info "Building Docker image: ${IMAGE_ANDROID}"
+ docker build \
+ --tag "$IMAGE_ANDROID" \
+ --file "${SCRIPT_DIR}/Dockerfile.android" \
+ "${SCRIPT_DIR}"
+ fi
+}
+
+# ── Common docker run wrapper ─────────────────────────────────────────────────
+# $1 = image name, $2 = inline bash commands to execute inside the container.
+# Source is copied to /tmp/build inside the container so bin/obj never pollute
+# the repo. Output is written to /output (→ host DIST_DIR sub-folder).
+run_build() {
+ local image="$1"
+ local commands="$2"
+ local out_dir="$3"
+
+ mkdir -p "$out_dir"
+
+ docker run --rm \
+ --volume "${PROJECT_ROOT}:/src:ro" \
+ --volume "${out_dir}:/output" \
+ --volume "${NUGET_VOLUME}:/root/.nuget/packages" \
+ "$image" \
+ bash -euo pipefail -c "
+ cp -r /src/. /tmp/build
+ cd /tmp/build
+ ${commands}
+ chown -R $(id -u):$(id -g) /output
+ "
+}
+
+# ── Per-target build functions ────────────────────────────────────────────────
+build_windows() {
+ ensure_desktop_image
+ info "Building Windows x64 …"
+ run_build "$IMAGE_DESKTOP" \
+ "dotnet publish src/App.Desktop \
+ -r win-x64 \
+ -c Release \
+ -p:PublishSingleFile=true \
+ -p:IncludeNativeLibrariesForSelfExtract=true \
+ --self-contained \
+ -o /tmp/win-out
+ cp /tmp/win-out/PalladiumWallet.exe \
+ \"/output/PalladiumWallet-${VERSION}-win-x64.exe\"" \
+ "${DIST_DIR}/windows"
+ ok "Windows → dist/windows/PalladiumWallet-${VERSION}-win-x64.exe"
+}
+
+build_linux() {
+ ensure_desktop_image
+ info "Building Linux x64 …"
+ run_build "$IMAGE_DESKTOP" \
+ "dotnet publish src/App.Desktop \
+ -r linux-x64 \
+ -c Release \
+ -p:PublishSingleFile=true \
+ -p:IncludeNativeLibrariesForSelfExtract=true \
+ --self-contained \
+ -o /tmp/linux-out
+ install -m 755 /tmp/linux-out/PalladiumWallet \
+ \"/output/PalladiumWallet-${VERSION}-linux-x64\"" \
+ "${DIST_DIR}/linux"
+ ok "Linux → dist/linux/PalladiumWallet-${VERSION}-linux-x64"
+}
+
+build_android() {
+ ensure_android_image
+ info "Building Android APK …"
+ run_build "$IMAGE_ANDROID" \
+ "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
+ dotnet build src/App.Android \
+ -c Release \
+ -t:SignAndroidPackage \
+ -p:AndroidSdkDirectory=\${ANDROID_HOME}
+ cp src/App.Android/bin/Release/net10.0-android/*-Signed.apk \
+ \"/output/PalladiumWallet-${VERSION}.apk\"" \
+ "${DIST_DIR}/android"
+ ok "Android → dist/android/PalladiumWallet-${VERSION}.apk"
+}
+
+# ── Dispatch ──────────────────────────────────────────────────────────────────
+START=$(date +%s)
+
+case "$TARGET" in
+ windows) build_windows ;;
+ linux) build_linux ;;
+ android) build_android ;;
+ all)
+ build_windows
+ build_linux
+ build_android
+ ;;
+esac
+
+ELAPSED=$(( $(date +%s) - START ))
+bold ""
+bold "Done in ${ELAPSED}s. Artifacts in: ${DIST_DIR}/"