Initial StartOS 0.3.5.1 packaging scaffold for Kamado Pool

Multi-stage Dockerfile clones KamadoPool at a pinned SHA, builds
ckpool and kamado-api (with embedded Svelte UI), runtime image
supervises both processes via tini + wait -n. Config covers
bitcoind mainnet/testnet4 variant, payout address, coinbase tag,
vardiff knobs, and log level. Web UI interface only — stratum
:3333 requires a router port-forward or simpleproxy workaround
because StartOS 0.3.x does not forward raw TCP on LAN.

TODO before first build: pin KAMADO_REPO + KAMADO_SHA in the
Dockerfile to a pushed commit.
This commit is contained in:
satoshi
2026-04-13 03:51:23 +03:00
commit 4064f9e56a
18 changed files with 689 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
*.s9pk
image.tar
scripts/embassy.js
.DS_Store
+87
View File
@@ -0,0 +1,87 @@
# syntax=docker/dockerfile:1.6
#
# Kamado Pool StartOS package image.
#
# Clones KamadoPool at a pinned SHA, builds the patched ckpool-solo
# binary, builds the Go api binary with embedded Svelte UI, then
# assembles a small runtime image that supervises both processes.
#
# TODO: set KAMADO_REPO + KAMADO_SHA to a pushed commit before
# running `make` — the defaults below are placeholders and the build
# WILL fail until the KamadoPool source repo is pushed to GitHub.
ARG KAMADO_REPO=https://github.com/Relaxo143/KamadoPool.git
ARG KAMADO_SHA=REPLACE_ME_WITH_PUSHED_COMMIT_SHA
ARG ARCH
# ---------- stage 1: fetch source ----------
FROM debian:bookworm-slim AS source
ARG KAMADO_REPO
ARG KAMADO_SHA
RUN apt-get update && apt-get install -y --no-install-recommends \
git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
RUN git clone "${KAMADO_REPO}" kamado \
&& cd kamado && git checkout "${KAMADO_SHA}"
# ---------- stage 2: build ckpool ----------
FROM debian:bookworm-slim AS ckpool-build
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool pkg-config \
yasm libzmq3-dev git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=source /src/kamado /src/kamado
WORKDIR /src/kamado
RUN make ckpool-src \
&& cd ckpool/src \
&& ./autogen.sh \
&& ./configure --without-ckdb --prefix=/usr/local \
&& make -j"$(nproc)" \
&& install -Dm755 src/ckpool /out/usr/local/bin/ckpool
# ---------- stage 3: build UI ----------
FROM node:22-bookworm-slim AS ui-build
WORKDIR /ui
COPY --from=source /src/kamado/ui/package.json ./
RUN npm install --no-audit --no-fund
COPY --from=source /src/kamado/ui/ ./
RUN npm run build
# ---------- stage 4: build Go api with embedded UI ----------
FROM golang:1.22-bookworm AS api-build
WORKDIR /src
COPY --from=source /src/kamado/api/ ./
RUN rm -rf internal/webui/dist && mkdir -p internal/webui/dist
COPY --from=ui-build /ui/dist/ internal/webui/dist/
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath -ldflags="-s -w" \
-o /out/kamado-api ./cmd/kamado-api
# ---------- stage 5: runtime ----------
FROM debian:bookworm-slim AS runtime
ARG ARCH
ARG YQ_VERSION=v4.44.3
RUN apt-get update && apt-get install -y --no-install-recommends \
tini ca-certificates curl libzmq5 \
&& rm -rf /var/lib/apt/lists/*
# yq for parsing StartOS config.yaml inside the entrypoint
RUN set -eu; \
case "${ARCH}" in \
x86_64|amd64) YQ_ARCH=amd64 ;; \
aarch64|arm64) YQ_ARCH=arm64 ;; \
*) echo "unsupported arch: ${ARCH}" >&2; exit 1 ;; \
esac; \
curl -fsSL -o /usr/local/bin/yq \
"https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_${YQ_ARCH}"; \
chmod +x /usr/local/bin/yq
COPY --from=ckpool-build /out/usr/local/bin/ckpool /usr/local/bin/ckpool
COPY --from=api-build /out/kamado-api /usr/local/bin/kamado-api
COPY docker_entrypoint.sh /usr/local/bin/docker_entrypoint.sh
RUN chmod +x /usr/local/bin/docker_entrypoint.sh
EXPOSE 3333 8080
WORKDIR /root
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/docker_entrypoint.sh"]
+22
View File
@@ -0,0 +1,22 @@
Kamado Pool
Copyright (C) 2026 Kamado Pool contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
---
This project is built on top of CKPool by Con Kolivas, which is itself
licensed under GPL-3.0. See https://bitbucket.org/ckolivas/ckpool for
the upstream source. The full GPL-3.0 text is available at:
https://www.gnu.org/licenses/gpl-3.0.txt
+31
View File
@@ -0,0 +1,31 @@
PKG_ID := $(shell yq -r .id manifest.yaml)
PKG_VERSION := $(shell yq -r .version manifest.yaml)
TS_FILES := $(shell find ./scripts -name \*.ts 2>/dev/null)
.DELETE_ON_ERROR:
all: verify
verify: $(PKG_ID).s9pk
start-sdk verify s9pk $(PKG_ID).s9pk
install: $(PKG_ID).s9pk
start-cli package install $(PKG_ID).s9pk
$(PKG_ID).s9pk: manifest.yaml instructions.md icon.png LICENSE scripts/embassy.js image.tar
start-sdk pack
scripts/embassy.js: $(TS_FILES)
deno run --allow-read --allow-write --allow-env --allow-net scripts/bundle.ts
image.tar: Dockerfile docker_entrypoint.sh
docker buildx build --tag start9/$(PKG_ID)/main:$(PKG_VERSION) \
--platform=linux/arm64/v8,linux/amd64 \
--build-arg ARCH=$(shell uname -m) \
-o type=oci,dest=image.tar .
clean:
rm -f $(PKG_ID).s9pk image.tar scripts/embassy.js
.PHONY: all verify install clean
+23
View File
@@ -0,0 +1,23 @@
# Kamado Pool — StartOS Packaging
StartOS 0.3.5.1 wrapper for [Kamado Pool](https://github.com/Relaxo143/KamadoPool), a modern solo Bitcoin mining pool built on a patched fork of CKPool-solo with a Go middleware API and Svelte real-time dashboard.
## Build
```sh
make
```
This runs `deno` to bundle the embassy TypeScript procedures, builds a multi-arch OCI image via `docker buildx`, and packs everything into `kamado-pool.s9pk` using `start-sdk`.
> **Note:** the Dockerfile clones the Kamado source from a pinned git SHA. Before the first build, set the `KAMADO_REPO` and `KAMADO_SHA` build args in [Dockerfile](Dockerfile) to a real pushed commit on the upstream repo.
## Install
```sh
make install
```
## License
GPL-3.0 — matches upstream Kamado and CKPool.
+100
View File
@@ -0,0 +1,100 @@
#!/bin/bash
# Kamado Pool StartOS entrypoint.
#
# Renders ckpool.conf from /root/.kamado/start9/config.yaml, resolves
# which bitcoind variant the user picked (mainnet vs testnet4), then
# supervises ckpool and kamado-api as a pair — if either exits, tear
# the container down so StartOS restarts the service cleanly.
set -euo pipefail
CONFIG_FILE="/root/.kamado/start9/config.yaml"
CKPOOL_DIR="/root/.ckpool"
CKPOOL_CONF="${CKPOOL_DIR}/ckpool.conf"
CKPOOL_LOG_DIR="${CKPOOL_DIR}/logs"
mkdir -p "${CKPOOL_DIR}" "${CKPOOL_LOG_DIR}"
if [[ ! -f "${CONFIG_FILE}" ]]; then
echo "kamado-entrypoint: config file missing: ${CONFIG_FILE}" >&2
exit 1
fi
q() { yq -r "$1" "${CONFIG_FILE}"; }
BITCOIND_VARIANT=$(q '.bitcoind.type')
case "${BITCOIND_VARIANT}" in
bitcoind)
BITCOIN_HOST="bitcoind.embassy"
BITCOIN_PORT=8332
;;
bitcoind-testnet)
BITCOIN_HOST="bitcoind-testnet.embassy"
BITCOIN_PORT=48332
;;
*)
echo "kamado-entrypoint: unknown bitcoind variant: ${BITCOIND_VARIANT}" >&2
exit 1
;;
esac
BITCOIN_USER=$(q '.bitcoind.user')
BITCOIN_PASS=$(q '.bitcoind.password')
POOL_BTCADDRESS=$(q '.pool-address')
POOL_IDENTIFIER=$(q '.pool-identifier')
DROPIDLE=$(q '.dropidle // 0')
STARTDIFF=$(q '.startdiff // 16384')
MINDIFF=$(q '.mindiff // 1000')
MAXDIFF=$(q '.maxdiff // 0')
LOG_LEVEL=$(q '.log-level // "info"')
cat > "${CKPOOL_CONF}" <<EOF
{
"btcd": [
{
"url": "${BITCOIN_HOST}:${BITCOIN_PORT}",
"auth": "${BITCOIN_USER}",
"pass": "${BITCOIN_PASS}",
"notify": true
}
],
"btcaddress": "${POOL_BTCADDRESS}",
"btcsig": "${POOL_IDENTIFIER}",
"serverurl": [
"0.0.0.0:3333"
],
"mindiff": ${MINDIFF},
"startdiff": ${STARTDIFF},
"maxdiff": ${MAXDIFF},
"logdir": "${CKPOOL_LOG_DIR}"
}
EOF
echo "kamado-entrypoint: starting ckpool (solo, ${BITCOIND_VARIANT})"
ckpool -B -c "${CKPOOL_CONF}" &
CKPOOL_PID=$!
export KAMADO_CKPOOL_SOCKET="${CKPOOL_DIR}/solo/listener"
export KAMADO_CKPOOL_LOGFILE="${CKPOOL_LOG_DIR}/ckpool.log"
export KAMADO_BITCOIND_URL="http://${BITCOIN_HOST}:${BITCOIN_PORT}"
export KAMADO_BITCOIND_USER="${BITCOIN_USER}"
export KAMADO_BITCOIND_PASS="${BITCOIN_PASS}"
export KAMADO_HTTP_ADDR=":8080"
export KAMADO_LOG_LEVEL="${LOG_LEVEL}"
echo "kamado-entrypoint: starting kamado-api"
kamado-api &
API_PID=$!
term() {
echo "kamado-entrypoint: SIGTERM — shutting down"
kill -TERM "${API_PID}" "${CKPOOL_PID}" 2>/dev/null || true
wait "${API_PID}" "${CKPOOL_PID}" 2>/dev/null || true
exit 0
}
trap term TERM INT
wait -n "${CKPOOL_PID}" "${API_PID}"
EXIT_CODE=$?
echo "kamado-entrypoint: one of ckpool/kamado-api exited (${EXIT_CODE}), stopping the other"
kill -TERM "${API_PID}" "${CKPOOL_PID}" 2>/dev/null || true
wait || true
exit "${EXIT_CODE}"
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

+30
View File
@@ -0,0 +1,30 @@
# Kamado Pool
Kamado is a solo Bitcoin mining pool built on a patched fork of CKPool-solo, with a Go middleware API and a real-time Svelte dashboard. When a miner connected to your Kamado instance solves a block, **the full block reward goes to the payout address you configured** — no pool fees, no splits, no share accounting.
## Setup
1. Install Bitcoin Core (mainnet) **or** Bitcoin Core (testnet4). Kamado supports either as a dependency; pick one in the Kamado config under *Bitcoin Core > Type*.
2. In the Kamado config, set **Payout Address** to the Bitcoin address that should receive solved block rewards.
3. Optionally adjust the **Coinbase Tag** (default `/Kamado/`) and the vardiff parameters.
4. Start Kamado. Open the web UI from the Services page to watch live hashrate, miners, best shares, and solved blocks.
## Connecting miners
Kamado's stratum server listens on TCP port **3333** inside the container. **StartOS 0.3.x does not forward raw TCP ports on the LAN interface**, so you have two options to reach stratum from miners on your local network:
- **Router port-forward**: Forward an external port on your router directly to your StartOS server's LAN IP on port 3333 and point miners at that.
- **simpleproxy on a second host**: Run `simpleproxy -L 3333 -R <startos-lan-ip>:3333` on any always-on LAN host and point miners at that host.
Once forwarding is in place, miners connect to `stratum+tcp://<forward-host>:3333` with the username set to **any label you like** (it becomes the worker name in the dashboard) and any password.
## Troubleshooting
- **No miners appear after connecting**: confirm the forward actually reaches the StartOS container — `telnet <forward-host> 3333` should connect. Check the Kamado logs in the Services page.
- **Bitcoin Core RPC errors**: make sure Bitcoin Core is fully synced and the RPC dependency was auto-configured (Kamado enables `rpc.enable` during setup).
- **Best share resets to 0 after a block is found**: upstream CKPool zeroes the "current round" best diff on solve. Kamado ships a patch that also exposes the all-time best, so the dashboard has both columns.
## Upstream
CKPool-solo by Con Kolivas: <https://bitbucket.org/ckolivas/ckpool>
Kamado source: <https://github.com/Relaxo143/KamadoPool>
+151
View File
@@ -0,0 +1,151 @@
id: kamado-pool
title: "Kamado Pool"
version: 0.1.0
release-notes: |
Initial StartOS 0.3.5.1 packaging for Kamado Pool — a modern solo
Bitcoin mining pool built on a patched fork of CKPool-solo with a
Go middleware API and Svelte real-time dashboard.
license: gpl-3.0
wrapper-repo: "https://github.com/Relaxo143/KamadoPool-StartOS"
upstream-repo: "https://github.com/Relaxo143/KamadoPool"
support-site: "https://github.com/Relaxo143/KamadoPool/issues"
marketing-site: "https://github.com/Relaxo143/KamadoPool"
donation-url: ~
build: ["make"]
description:
short: "Modern solo Bitcoin mining pool with real-time dashboard"
long: |
Kamado Pool is a solo Bitcoin mining pool built on a patched fork
of CKPool-solo. Unlike wrappers that read periodic stats files,
Kamado talks directly to CKPool's Unix socket API to expose real-
time per-client hashrate, difficulty, hardware detection, full
block history, and best-share leaderboards (both current round
and all-time) via a Svelte dashboard with WebSocket push.
Stratum listens on port 3333. Because StartOS 0.3.x does not
forward raw TCP ports on the LAN interface, you must use a
simpleproxy or router port-forward to reach the stratum endpoint
from miners on your local network. See instructions.
assets:
license: LICENSE
icon: icon.png
instructions: instructions.md
docker-images: image.tar
main:
type: docker
image: main
entrypoint: docker_entrypoint.sh
args: []
mounts:
main: /root/.kamado
ckpool: /root/.ckpool
compat: /mnt/assets
gpu-acceleration: false
hardware-requirements:
arch:
- x86_64
- aarch64
health-checks:
web:
name: "Web Dashboard"
success-message: "Kamado dashboard is reachable."
type: script
config:
get:
type: script
set:
type: script
properties:
type: script
volumes:
main:
type: data
ckpool:
type: data
compat:
type: assets
interfaces:
main:
name: Web Dashboard
description: "Real-time Kamado Pool dashboard (hashrate, miners, blocks, best shares)."
tor-config:
port-mapping:
80: "8080"
lan-config:
443:
ssl: true
internal: 8080
ui: true
protocols:
- tcp
- http
dependencies:
bitcoind:
version: ">=24.0.0 <30.0.0"
requirement:
type: "opt-in"
how: "Can alternatively use bitcoind-testnet for testnet4 testing."
description: "Used to submit found blocks and receive new block templates via RPC + ZMQ."
config:
check:
type: script
auto-configure:
type: script
bitcoind-testnet:
version: ">=24.0.0 <30.0.0"
requirement:
type: "opt-in"
how: "Can alternatively use mainnet bitcoind."
description: "Testnet4 variant of bitcoind for testing Kamado without real funds."
config:
check:
type: script
auto-configure:
type: script
backup:
create:
type: docker
image: compat
system: true
entrypoint: compat
args:
- duplicity
- create
- /mnt/backup
- /root/.kamado
mounts:
BACKUP: /mnt/backup
main: /root/.kamado
restore:
type: docker
image: compat
system: true
entrypoint: compat
args:
- duplicity
- restore
- /mnt/backup
- /root/.kamado
mounts:
BACKUP: /mnt/backup
main: /root/.kamado
migrations:
from:
"*":
type: script
args: ["from"]
to:
"*":
type: script
args: ["to"]
+4
View File
@@ -0,0 +1,4 @@
import { bundle } from "https://deno.land/x/emit@0.40.0/mod.ts";
const result = await bundle(new URL("./embassy.ts", import.meta.url));
await Deno.writeTextFile("scripts/embassy.js", result.code);
+1
View File
@@ -0,0 +1 @@
export * from "https://deno.land/x/embassyd_sdk@v0.3.3.0.11/mod.ts";
+6
View File
@@ -0,0 +1,6 @@
export { getConfig } from "./procedures/getConfig.ts";
export { setConfig } from "./procedures/setConfig.ts";
export { dependencies } from "./procedures/dependencies.ts";
export { health } from "./procedures/healthChecks.ts";
export { migration } from "./procedures/migrations.ts";
export { properties } from "./procedures/properties.ts";
+45
View File
@@ -0,0 +1,45 @@
import { matches, types as T } from "../deps.ts";
const { shape, boolean, arrayOf, string, dictionary, any } = matches;
const bitcoindMatcher = shape({
"rpc": shape({
"enable": boolean,
"username": string,
"password": string,
}, ["enable", "username", "password"]),
"advanced": shape({
"peers": shape({
"listen": boolean,
}, ["listen"]),
}, ["peers"]),
}, ["rpc", "advanced"]);
const checkBitcoind: T.ExpectedExports.dependencyConfig["check"] = async (
effects,
input,
) => {
const res = bitcoindMatcher.test(input);
if (!res) return { error: "Bitcoin Core RPC settings are missing." };
if (!input.rpc.enable) {
return { error: "Bitcoin Core RPC must be enabled." };
}
return { result: null };
};
const autoBitcoind: T.ExpectedExports.dependencyConfig["autoConfigure"] =
async (effects, input) => {
input.rpc.enable = true;
return { result: input };
};
export const dependencies: T.ExpectedExports.dependencies = {
"bitcoind": {
check: checkBitcoind,
autoConfigure: autoBitcoind,
},
"bitcoind-testnet": {
check: checkBitcoind,
autoConfigure: autoBitcoind,
},
};
+135
View File
@@ -0,0 +1,135 @@
import { compat, types as T } from "../deps.ts";
export const getConfig: T.ExpectedExports.getConfig = compat.getConfig({
"bitcoind": {
"type": "union",
"name": "Bitcoin Core",
"description": "Select which Bitcoin Core node Kamado should use.",
"tag": {
"id": "type",
"name": "Type",
"description": "Mainnet bitcoind or the testnet4 variant.",
"variant-names": {
"bitcoind": "Bitcoin Core (mainnet)",
"bitcoind-testnet": "Bitcoin Core (testnet4)",
},
},
"default": "bitcoind",
"variants": {
"bitcoind": {
"user": {
"type": "pointer",
"name": "RPC Username",
"description": "The RPC username from Bitcoin Core.",
"subtype": "package",
"package-id": "bitcoind",
"target": "config",
"multi": false,
"selector": "$.rpc.username",
},
"password": {
"type": "pointer",
"name": "RPC Password",
"description": "The RPC password from Bitcoin Core.",
"subtype": "package",
"package-id": "bitcoind",
"target": "config",
"multi": false,
"selector": "$.rpc.password",
},
},
"bitcoind-testnet": {
"user": {
"type": "pointer",
"name": "RPC Username",
"description": "The RPC username from Bitcoin Core (testnet4).",
"subtype": "package",
"package-id": "bitcoind-testnet",
"target": "config",
"multi": false,
"selector": "$.rpc.username",
},
"password": {
"type": "pointer",
"name": "RPC Password",
"description": "The RPC password from Bitcoin Core (testnet4).",
"subtype": "package",
"package-id": "bitcoind-testnet",
"target": "config",
"multi": false,
"selector": "$.rpc.password",
},
},
},
},
"pool-address": {
"type": "string",
"name": "Payout Address",
"description":
"Bitcoin address that receives the full block reward when a block is solved. Solo mining — no fees, no splits.",
"nullable": false,
"masked": false,
"copyable": true,
},
"pool-identifier": {
"type": "string",
"name": "Coinbase Tag",
"description":
"Short string embedded in the coinbase transaction of solved blocks. Max 32 characters.",
"nullable": false,
"default": "/Kamado/",
"masked": false,
"copyable": false,
},
"startdiff": {
"type": "number",
"name": "Starting Difficulty",
"description":
"Initial vardiff target for new miner connections. Bitaxe-class miners typically land around 16384.",
"nullable": false,
"default": 16384,
"range": "[1,*)",
"integral": true,
},
"mindiff": {
"type": "number",
"name": "Minimum Difficulty",
"description": "Floor for the vardiff algorithm.",
"nullable": false,
"default": 1000,
"range": "[1,*)",
"integral": true,
},
"maxdiff": {
"type": "number",
"name": "Maximum Difficulty",
"description": "Ceiling for the vardiff algorithm. 0 means no cap.",
"nullable": false,
"default": 0,
"range": "[0,*)",
"integral": true,
},
"dropidle": {
"type": "number",
"name": "Drop Idle (seconds)",
"description":
"Disconnect clients that have not submitted a share in this many seconds. 0 disables the idle disconnect.",
"nullable": false,
"default": 0,
"range": "[0,*)",
"integral": true,
},
"log-level": {
"type": "enum",
"name": "Log Level",
"description": "Verbosity of the kamado-api log output.",
"values": ["debug", "info", "warn", "error"],
"value-names": {
"debug": "Debug",
"info": "Info",
"warn": "Warn",
"error": "Error",
},
"default": "info",
},
});
+10
View File
@@ -0,0 +1,10 @@
import { healthUtil, types as T } from "../deps.ts";
export const health: T.ExpectedExports.health = {
"web": async (effects, duration) => {
return healthUtil.checkWebUrl("http://kamado-pool.embassy:8080/api/health")(
effects,
duration,
);
},
};
+4
View File
@@ -0,0 +1,4 @@
import { compat, types as T } from "../deps.ts";
export const migration: T.ExpectedExports.migration = compat.migrations
.fromMapping({}, "0.1.0");
+22
View File
@@ -0,0 +1,22 @@
import { types as T, YAML } from "../deps.ts";
const noProps: T.ExpectedExports.properties = async () => {
return {
result: {
version: 2,
data: {
"Dashboard": {
type: "string",
value:
"Open the Kamado web UI from the Services page for live stats.",
description: "Kamado exposes everything through the web dashboard.",
copyable: false,
qr: false,
masked: false,
},
},
},
};
};
export const properties = noProps;
+14
View File
@@ -0,0 +1,14 @@
import { compat, types as T } from "../deps.ts";
export const setConfig: T.ExpectedExports.setConfig = compat.setConfig({
"bitcoind": {
"bitcoind": [{
"name": "bitcoind",
"version": ">=24.0.0 <30.0.0",
}],
"bitcoind-testnet": [{
"name": "bitcoind-testnet",
"version": ">=24.0.0 <30.0.0",
}],
},
});