Teach the installer to detect and configure Docker or Podman.
Release rolling / release (push) Successful in 7m55s

Auto-select the engine, set socket group permissions (including Debian
podman.socket drop-ins), write PEARDOCK_ENGINE into .env, and generate a
matching systemd unit for the chosen runtime.
This commit is contained in:
Raven Scott
2026-07-16 05:49:08 -04:00
parent 69d117b7ed
commit fda8695fbe
4 changed files with 654 additions and 94 deletions
+3 -1
View File
@@ -34,8 +34,10 @@ curl -fsSL https://install.peardock.boats | bash
Non-interactive:
```bash
# Server (Linux → /opt/peardock + peardock.service + docker group)
# Server (Linux → /opt/peardock + peardock.service; auto-detects Docker or Podman)
curl -fsSL https://install.peardock.boats | bash -s -- --server --yes
# Force Podman (rootful socket + group permissions):
# curl -fsSL https://install.peardock.boats | bash -s -- --server --yes --engine podman
# Desktop client (Linux or macOS)
curl -fsSL https://install.peardock.boats | bash -s -- --client --yes
+13 -8
View File
@@ -1,11 +1,12 @@
[Unit]
Description=peardock HyperDHT Docker control plane
Description=peardock HyperDHT container control plane
Documentation=https://git.ssh.surf/snxraven/peardock
After=network-online.target
Wants=network-online.target
# Soft-depend on Docker / Podman (either socket is enough)
# Soft-depend on Docker and/or Podman (installer rewrites After=/Wants= for the selected engine)
Wants=docker.service
# Rootful Podman API (optional; ignore if unit missing)
# Rootful Podman API — uncomment or let install.sh set this when PEARDOCK_ENGINE=podman:
# After=podman.socket
# Wants=podman.socket
[Service]
@@ -24,22 +25,26 @@ NoNewPrivileges=true
PrivateTmp=true
User=peardock
# install.sh sets Group/SupplementaryGroups to the socket group (docker or podman)
Group=docker
SupplementaryGroups=docker
Environment=NODE_ENV=production
EnvironmentFile=-/opt/peardock/.env
# Installer also writes PEARDOCK_ENGINE / DOCKER_HOST into .env and may set:
# Environment=PEARDOCK_ENGINE=podman
# Environment=DOCKER_HOST=unix:///run/podman/podman.sock
# Environment=CONTAINER_HOST=unix:///run/podman/podman.sock
# Environment=DOCKER_API_VERSION=v1.41
#
# Example production knobs (prefer .env):
# Environment=PEARDOCK_DEFAULT_ROLE=operator
# Environment=PEARDOCK_PEER_ALLOWLIST=1
# Environment=PEARDOCK_ENGINE=podman
# Environment=DOCKER_HOST=unix:///run/podman/podman.sock
# Swarm + Holesail are on by default (Docker); Swarm is always off on Podman
# Environment=ENABLE_SWARM=0
# Environment=ENABLE_HOLESAIL=0
# Environment=DOCKER_API_VERSION=v1.41
ReadWritePaths=/opt/peardock
# State dir + common engine sockets (Docker + Podman rootful)
ReadWritePaths=/opt/peardock /var/run/docker.sock /run/docker.sock /run/podman/podman.sock /var/run/podman/podman.sock
[Install]
WantedBy=multi-user.target
+52 -6
View File
@@ -6,19 +6,52 @@ Architecture and code docs: [README.md](./README.md) (hub) · [ARCHITECTURE.md](
## Recommended: one-line installer
On a Linux host with Docker already installed:
On a Linux host with **Docker and/or Podman**:
```bash
curl -fsSL https://install.peardock.boats | bash -s -- --server --yes
```
This downloads the Bare server binary from the **`rolling`** release, creates user `peardock`, adds it to the **docker** group, writes `/etc/systemd/system/peardock.service`, and can enable the service.
The installer:
1. Downloads the Bare server binary from the **`rolling`** release
2. Creates system user `peardock`
3. **Detects** Docker and Podman sockets (and can enable `docker` / `podman.socket`)
4. Configures **socket group permissions** (docker or podman) for Debian/Ubuntu, RHEL/Fedora, etc.
5. Writes `/opt/peardock/.env` with `PEARDOCK_ENGINE` + `DOCKER_HOST`
6. Installs `/etc/systemd/system/peardock.service` with the correct group and socket paths
7. Optionally enables and starts the service
```bash
# Prefer Docker (default auto order: Docker → Podman)
curl -fsSL https://install.peardock.boats | bash -s -- --server --yes --engine docker
# Prefer / force Podman (rootful socket + group drop-in)
curl -fsSL https://install.peardock.boats | bash -s -- --server --yes --engine podman
# or: PEARDOCK_ENGINE=podman bash … --server --yes
```
```bash
sudo systemctl status peardock
sudo journalctl -u peardock -f
```
### Debian / Ubuntu notes
| Engine | Packages / enable | Installer behavior |
|--------|-------------------|--------------------|
| Docker | `sudo apt install docker.io` then `sudo systemctl enable --now docker` | Adds `peardock` to group **`docker`**, socket usually `/var/run/docker.sock` |
| Podman | `sudo apt install podman` then socket unit available | Enables **`podman.socket`**, creates group **`podman`**, installs drop-in `SocketGroup=podman` + `SocketMode=0660` under `/etc/systemd/system/podman.socket.d/peardock.conf`, adds `peardock` to `podman` |
Without the drop-in, rootful Podman sockets are often `root:root` mode `660` and the `peardock` user cannot connect.
```bash
sudo systemctl status peardock
sudo journalctl -u peardock -f
# Engine wiring:
sudo grep -E '^(PEARDOCK_ENGINE|DOCKER_HOST)=' /opt/peardock/.env
```
### Keys: check `/opt/peardock/.env`
On first successful start the server writes identity into the env file (mode 600):
@@ -54,16 +87,29 @@ Flags and Windows notes: [peardock.boats/download](https://peardock.boats/downlo
Unit source of truth: [`deploy/peardock.service`](../deploy/peardock.service).
## Docker group / socket access
## Engine group / socket access
Engine sockets are typically `root:docker` mode `660`. The service **must** run as a user in group `docker` (or the sockets group).
Engine sockets are typically mode `660` and owned by a dedicated group. The service **must** run as a user in that group.
| Engine | Typical socket | Typical group |
|--------|----------------|---------------|
| Docker | `/var/run/docker.sock` | `docker` |
| Podman (rootful) | `/run/podman/podman.sock` | `podman` (installer drop-in) |
```bash
# Installer does this; manual recovery:
# Installer does this; manual recovery — Docker:
sudo usermod -aG docker peardock
# if docker group was just created, restart Docker once
sudo systemctl restart docker
sudo systemctl restart peardock
# Manual recovery — Podman (rootful):
sudo groupadd --system podman 2>/dev/null || true
sudo usermod -aG podman peardock
sudo mkdir -p /etc/systemd/system/podman.socket.d
# SocketGroup=podman + SocketMode=0660 (see install.sh)
sudo systemctl daemon-reload
sudo systemctl enable --now podman.socket
sudo systemctl restart peardock
```
Symptoms of a missing group: empty container lists, start failures, “permission denied” on the socket.
+587 -80
View File
@@ -17,6 +17,7 @@
# PEARDOCK_SERVER_DIR=/opt/peardock
# PEARDOCK_CLIENT_DIR=... (default: ~/.local/share/peardock or ~/Applications)
# PEARDOCK_YES=1 — assume defaults / non-interactive where possible
# PEARDOCK_ENGINE=auto|docker|podman — container engine preference (server)
#
# Canonical install host: https://install.peardock.boats (serves this script)
# Site: https://peardock.boats
@@ -35,6 +36,15 @@ SERVER_DIR="${PEARDOCK_SERVER_DIR:-/opt/peardock}"
TMPDIR_ROOT="${TMPDIR:-/tmp}"
INSTALL_TMP=""
CHANNEL="${PEARDOCK_CHANNEL:-stable}" # reserved
# auto | docker | podman — resolved during server install
ENGINE_PREF="${PEARDOCK_ENGINE:-auto}"
# Filled by detect_container_engines / ensure_container_engine_access
PEARDOCK_ENGINE_KIND=""
PEARDOCK_ENGINE_SOCK=""
PEARDOCK_ENGINE_GROUP=""
PEARDOCK_ENGINE_LABEL=""
PEARDOCK_DISTRO=""
PEARDOCK_DISTRO_LIKE=""
# ─── colors / UI ────────────────────────────────────────────────────────────
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then
@@ -114,6 +124,10 @@ parse_args() {
--tag) TAG="${2:-}"; shift 2 ;;
--version) VERSION="${2:-}"; shift 2 ;;
--server-dir) SERVER_DIR="${2:-}"; shift 2 ;;
--engine)
ENGINE_PREF="${2:-auto}"
shift 2
;;
--help|-h)
cat <<EOF
Usage: install.sh [options]
@@ -125,8 +139,13 @@ Usage: install.sh [options]
--tag TAG Release tag (default: rolling)
--version VER Pin asset version (default: auto from release)
--server-dir DIR Server install path (default: /opt/peardock)
--engine KIND Container engine: auto (default), docker, or podman
--help This help
Env:
PEARDOCK_ENGINE=auto|docker|podman
PEARDOCK_YES=1 non-interactive
One-liner:
curl -fsSL https://install.peardock.boats | bash
EOF
@@ -263,48 +282,274 @@ asset_url() {
echo "${GITEA_URL}/${OWNER}/${REPO}/releases/download/${TAG}/${name}"
}
# ─── docker group / socket access ───────────────────────────────────────────
# Engine socket is typically root:docker mode 660. peardock.service runs as
# User=peardock and must be a docker group member (SupplementaryGroups=docker).
ensure_peardock_docker_group() {
local sock="${DOCKER_HOST:-}"
local grp="docker"
# Normalize DOCKER_HOST=unix:///var/run/docker.sock → path
if [[ -z "$sock" || "$sock" == unix://* || "$sock" == /* ]]; then
sock="${sock#unix://}"
sock="${sock:-/var/run/docker.sock}"
else
# tcp://… — group membership not required for local socket, but still useful
sock="/var/run/docker.sock"
# ─── distro / container engine detection ─────────────────────────────────────
detect_distro() {
PEARDOCK_DISTRO="unknown"
PEARDOCK_DISTRO_LIKE=""
if [[ -r /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
PEARDOCK_DISTRO="${ID:-unknown}"
PEARDOCK_DISTRO_LIKE="${ID_LIKE:-}"
elif [[ -r /etc/debian_version ]]; then
PEARDOCK_DISTRO="debian"
PEARDOCK_DISTRO_LIKE="debian"
fi
export PEARDOCK_DISTRO PEARDOCK_DISTRO_LIKE
}
if [[ ! -S "$sock" ]]; then
warn "Docker socket not found at ${sock}"
warn "Install/start Docker, then re-run: sudo usermod -aG docker peardock && sudo systemctl restart peardock"
is_debian_family() {
case "${PEARDOCK_DISTRO}" in
debian|ubuntu|raspbian|linuxmint|pop|elementary|zorin|kali|parrot) return 0 ;;
esac
case " ${PEARDOCK_DISTRO_LIKE} " in
*" debian "*|*" ubuntu "*) return 0 ;;
esac
return 1
}
is_rhel_family() {
case "${PEARDOCK_DISTRO}" in
rhel|centos|fedora|rocky|almalinux|ol|amzn|nobara) return 0 ;;
esac
case " ${PEARDOCK_DISTRO_LIKE} " in
*" rhel "*|*" fedora "*|*" centos "*) return 0 ;;
esac
return 1
}
nologin_shell() {
for s in /usr/sbin/nologin /sbin/nologin /bin/false /usr/bin/nologin; do
if [[ -x "$s" ]]; then
echo "$s"
return
fi
done
echo /bin/false
}
# Normalize DOCKER_HOST / path to a bare unix path (or empty).
normalize_unix_sock() {
local raw="${1:-}"
raw="${raw#unix://}"
if [[ -z "$raw" ]]; then
echo ""
return
fi
if [[ "$raw" == /* || "$raw" == //./pipe/* ]]; then
echo "$raw"
return
fi
echo ""
}
socket_exists() {
local p="$1"
[[ -n "$p" && -S "$p" ]]
}
# Populate candidate Docker socket paths (existing first later).
list_docker_sock_candidates() {
local -a c=()
local n
n="$(normalize_unix_sock "${DOCKER_HOST:-}")"
[[ -n "$n" ]] && c+=("$n")
n="$(normalize_unix_sock "${DOCKER_SOCK:-}")"
[[ -n "$n" ]] && c+=("$n")
c+=(
/var/run/docker.sock
/run/docker.sock
)
if [[ -n "${XDG_RUNTIME_DIR:-}" ]]; then
c+=("${XDG_RUNTIME_DIR}/docker.sock")
fi
if [[ -n "${HOME:-}" ]]; then
c+=("${HOME}/.docker/run/docker.sock")
fi
printf '%s\n' "${c[@]}" | awk 'NF && !seen[$0]++'
}
list_podman_sock_candidates() {
local -a c=()
local n uid
n="$(normalize_unix_sock "${CONTAINER_HOST:-}")"
[[ -n "$n" && "$n" == *podman* ]] && c+=("$n")
n="$(normalize_unix_sock "${PODMAN_SOCK:-}")"
[[ -n "$n" ]] && c+=("$n")
c+=(
/run/podman/podman.sock
/var/run/podman/podman.sock
)
if [[ -n "${XDG_RUNTIME_DIR:-}" ]]; then
c+=("${XDG_RUNTIME_DIR}/podman/podman.sock")
fi
uid="$(id -u 2>/dev/null || true)"
if [[ -n "$uid" ]]; then
c+=("/run/user/${uid}/podman/podman.sock")
fi
if [[ -n "${HOME:-}" ]]; then
c+=(
"${HOME}/.local/share/containers/podman/machine/podman.sock"
"${HOME}/.local/share/containers/podman/machine/podman-machine-default/podman.sock"
)
fi
printf '%s\n' "${c[@]}" | awk 'NF && !seen[$0]++'
}
find_first_socket() {
local p
while IFS= read -r p; do
[[ -z "$p" ]] && continue
if socket_exists "$p"; then
echo "$p"
return 0
fi
done
return 1
}
# Prefer the socket's group when it is not root
local sock_grp
sock_grp="$(stat -c '%G' "$sock" 2>/dev/null || stat -f '%Sg' "$sock" 2>/dev/null || echo docker)"
if [[ -n "$sock_grp" && "$sock_grp" != "root" ]]; then
grp="$sock_grp"
have_cmd() { command -v "$1" >/dev/null 2>&1; }
# Try to enable rootful Podman API socket (system unit).
try_enable_podman_system_socket() {
if ! have_cmd systemctl; then
return 1
fi
# Prefer system (rootful) socket for the peardock system service user
if systemctl list-unit-files podman.socket >/dev/null 2>&1 \
|| [[ -f /lib/systemd/system/podman.socket || -f /usr/lib/systemd/system/podman.socket ]]; then
log "Enabling system podman.socket (rootful API)…"
run_root systemctl enable --now podman.socket 2>/dev/null || \
run_root systemctl start podman.socket 2>/dev/null || true
# Give systemd a moment to create the socket
sleep 0.5
return 0
fi
return 1
}
if ! getent group "$grp" >/dev/null 2>&1; then
log "Creating group '${grp}' for Docker socket access…"
if command -v groupadd >/dev/null 2>&1; then
try_enable_docker() {
if ! have_cmd systemctl; then
return 1
fi
if systemctl list-unit-files docker.service >/dev/null 2>&1 \
|| [[ -f /lib/systemd/system/docker.service || -f /usr/lib/systemd/system/docker.service ]]; then
log "Ensuring docker.service is running…"
run_root systemctl enable --now docker.service 2>/dev/null || \
run_root systemctl start docker.service 2>/dev/null || true
sleep 0.5
return 0
fi
# Debian docker.io sometimes uses docker.socket
if systemctl list-unit-files docker.socket >/dev/null 2>&1; then
run_root systemctl enable --now docker.socket 2>/dev/null || true
sleep 0.5
fi
return 0
}
ensure_group_exists() {
local grp="$1"
if getent group "$grp" >/dev/null 2>&1; then
return 0
fi
log "Creating group '${grp}'…"
if have_cmd groupadd; then
# Debian: --system is fine; some older systems reject flags
run_root groupadd --system "$grp" 2>/dev/null || run_root groupadd "$grp"
elif command -v addgroup >/dev/null 2>&1; then
elif have_cmd addgroup; then
# Debian addgroup --system
run_root addgroup --system "$grp" 2>/dev/null || run_root addgroup "$grp"
else
die "Cannot create group '${grp}' (need groupadd/addgroup)"
fi
}
add_user_to_group() {
local user="$1" grp="$2"
log "Adding user ${user} to group '${grp}'…"
if have_cmd usermod; then
run_root usermod -aG "$grp" "$user"
elif have_cmd gpasswd; then
run_root gpasswd -a "$user" "$grp"
elif have_cmd adduser; then
# Debian: adduser USER GROUP
run_root adduser "$user" "$grp" 2>/dev/null || run_root adduser "$user" "$grp"
else
die "Cannot add ${user} to ${grp} (need usermod/gpasswd/adduser)"
fi
if getent group "$grp" | grep -qE "(^|,)${user}(,|$)"; then
ok "${user} is a member of group '${grp}'"
return 0
fi
if run_root id -nG "$user" 2>/dev/null | tr ' ' '\n' | grep -qx "$grp"; then
ok "${user} is a member of group '${grp}'"
return 0
fi
die "Failed to add ${user} to group '${grp}'. Run: sudo usermod -aG ${grp} ${user}"
}
# Configure rootful Podman socket group access for peardock (Debian/RHEL/etc.).
# Stock podman.socket is often root:root 660 — peardock cannot use it without a drop-in.
configure_podman_socket_permissions() {
local sock="$1"
local grp="${2:-podman}"
ensure_group_exists "$grp"
# systemd drop-in so the socket keeps group + mode across restarts
if have_cmd systemctl && {
[[ -f /lib/systemd/system/podman.socket ]] \
|| [[ -f /usr/lib/systemd/system/podman.socket ]] \
|| systemctl list-unit-files podman.socket >/dev/null 2>&1
}; then
local drop_dir="/etc/systemd/system/podman.socket.d"
log "Installing podman.socket drop-in (SocketGroup=${grp}, mode 0660)…"
run_root mkdir -p "$drop_dir"
run_root tee "${drop_dir}/peardock.conf" >/dev/null <<EOF
# Managed by peardock installer — allow group '${grp}' to use the rootful API
[Socket]
SocketMode=0660
SocketGroup=${grp}
EOF
run_root systemctl daemon-reload
run_root systemctl restart podman.socket 2>/dev/null || \
run_root systemctl start podman.socket 2>/dev/null || true
sleep 0.5
fi
# Live socket: chgrp/chmod even if not managed by our drop-in yet
if socket_exists "$sock"; then
run_root chgrp "$grp" "$sock" 2>/dev/null || true
run_root chmod 660 "$sock" 2>/dev/null || true
fi
# Debian/Ubuntu sometimes use /var/run → /run symlink; touch both names
if [[ "$sock" == /run/podman/podman.sock && -S /var/run/podman/podman.sock ]]; then
run_root chgrp "$grp" /var/run/podman/podman.sock 2>/dev/null || true
run_root chmod 660 /var/run/podman/podman.sock 2>/dev/null || true
fi
}
configure_docker_socket_permissions() {
local sock="$1"
local grp="${2:-docker}"
ensure_group_exists "$grp"
local sock_grp mode
sock_grp="$(stat -c '%G' "$sock" 2>/dev/null || stat -f '%Sg' "$sock" 2>/dev/null || echo "$grp")"
if [[ -n "$sock_grp" && "$sock_grp" != "root" ]]; then
grp="$sock_grp"
fi
# Prefer socket's actual group if non-root
if [[ "$sock_grp" != "root" && -n "$sock_grp" ]]; then
ensure_group_exists "$sock_grp"
grp="$sock_grp"
fi
# Ensure socket is group-accessible when possible
local mode
mode="$(stat -c '%a' "$sock" 2>/dev/null || echo "")"
if [[ -n "$mode" && "$mode" != "666" && "$mode" != "660" && "$mode" != "620" ]]; then
log "Setting ${sock} group to ${grp}, mode 660…"
@@ -314,48 +559,247 @@ ensure_peardock_docker_group() {
run_root chgrp "$grp" "$sock" 2>/dev/null || true
fi
log "Adding user peardock to group '${grp}'…"
if command -v usermod >/dev/null 2>&1; then
run_root usermod -aG "$grp" peardock
elif command -v gpasswd >/dev/null 2>&1; then
run_root gpasswd -a peardock "$grp"
elif command -v adduser >/dev/null 2>&1; then
run_root adduser peardock "$grp" 2>/dev/null || run_root adduser peardock "$grp"
else
die "Cannot add peardock to ${grp} (need usermod/gpasswd/adduser)"
# Export resolved group
PEARDOCK_ENGINE_GROUP="$grp"
}
verify_socket_access() {
local sock="$1" grp="$2" label="$3"
if ! socket_exists "$sock"; then
warn "${label} socket missing at ${sock}"
return 1
fi
# Verify membership (id may need a moment; read /etc/group directly)
if getent group "$grp" | grep -qE '(^|,)peardock(,|$)'; then
ok "peardock is a member of group '${grp}'"
else
# usermod sometimes succeeds but getent lag — check id -nG as root
if run_root id -nG peardock 2>/dev/null | tr ' ' '\n' | grep -qx "$grp"; then
ok "peardock is a member of group '${grp}'"
else
die "Failed to add peardock to group '${grp}'. Run: sudo usermod -aG ${grp} peardock"
fi
fi
# Prove socket is reachable as peardock (sg runs with supplementary group)
if command -v sg >/dev/null 2>&1; then
# Prefer testing with the group active (new membership not always in current session)
if have_cmd sg; then
if run_root sg "$grp" -c "runuser -u peardock -- test -r '$sock' -a -w '$sock'" 2>/dev/null \
|| run_root runuser -u peardock -- sg "$grp" -c "test -r '$sock' -a -w '$sock'" 2>/dev/null; then
ok "Docker socket ${sock} is read/write for peardock (via ${grp})"
else
# fallback: test as peardock with full group list after usermod
|| run_root runuser -u peardock -- sg "$grp" -c "test -r '$sock' -a -w '$sock'" 2>/dev/null \
|| run_root su -s /bin/sh peardock -c "sg $grp -c \"test -r '$sock' -a -w '$sock'\"" 2>/dev/null; then
ok "${label} socket ${sock} is read/write for peardock (via ${grp})"
return 0
fi
fi
if run_root su -s /bin/sh peardock -c "test -r '$sock' -a -w '$sock'" 2>/dev/null; then
ok "Docker socket ${sock} is accessible to peardock"
ok "${label} socket ${sock} is accessible to peardock"
return 0
fi
warn "Could not verify ${label} socket RW as peardock yet"
warn " After install: sudo systemctl restart peardock"
if [[ "$label" == "Docker" ]]; then
warn " sudo systemctl restart docker"
else
warn "Could not verify socket RW as peardock yet — restart Docker after group change if needed"
warn " sudo systemctl restart docker && sudo systemctl restart peardock"
warn " sudo systemctl restart podman.socket"
fi
return 1
}
# Merge PEARDOCK_ENGINE / DOCKER_HOST into server .env without clobbering secrets.
write_engine_env() {
local env_file="${SERVER_DIR}/.env"
local kind="$1" sock="$2"
local unix="unix://${sock}"
local tmp keep
run_root touch "$env_file"
run_root chmod 600 "$env_file"
tmp="$(mktemp)"
keep="$(mktemp)"
# Preserve existing lines except engine-related keys we manage
if run_root cat "$env_file" 2>/dev/null >"$keep"; then
grep -vE '^(PEARDOCK_ENGINE|DOCKER_HOST|CONTAINER_HOST|DOCKER_API_VERSION)=' "$keep" >"$tmp" || true
else
: >"$tmp"
fi
rm -f "$keep"
{
echo "PEARDOCK_ENGINE=${kind}"
echo "DOCKER_HOST=${unix}"
echo "CONTAINER_HOST=${unix}"
if [[ "$kind" == "podman" ]]; then
echo "DOCKER_API_VERSION=v1.41"
fi
} >>"$tmp"
run_root cp "$tmp" "$env_file"
run_root chmod 600 "$env_file"
run_root chown peardock:peardock "$env_file" 2>/dev/null || run_root chown peardock "$env_file" || true
rm -f "$tmp"
ok "Wrote engine settings to ${env_file} (PEARDOCK_ENGINE=${kind})"
}
# Detect Docker and/or Podman; pick engine from PEARDOCK_ENGINE / --engine / auto.
detect_container_engines() {
detect_distro
log "Distro: ${PEARDOCK_DISTRO}${PEARDOCK_DISTRO_LIKE:+ (like ${PEARDOCK_DISTRO_LIKE})}"
local docker_sock="" podman_sock=""
local has_docker_bin=0 has_podman_bin=0
have_cmd docker && has_docker_bin=1
have_cmd podman && has_podman_bin=1
docker_sock="$(list_docker_sock_candidates | find_first_socket || true)"
podman_sock="$(list_podman_sock_candidates | find_first_socket || true)"
# Soft-start engines when binaries exist but sockets are down
if [[ -z "$docker_sock" && "$has_docker_bin" -eq 1 ]]; then
try_enable_docker || true
docker_sock="$(list_docker_sock_candidates | find_first_socket || true)"
fi
if [[ -z "$podman_sock" && "$has_podman_bin" -eq 1 ]]; then
try_enable_podman_system_socket || true
podman_sock="$(list_podman_sock_candidates | find_first_socket || true)"
fi
# Explicit DOCKER_HOST from environment wins as preferred path for classification
local env_host
env_host="$(normalize_unix_sock "${DOCKER_HOST:-${CONTAINER_HOST:-}}")"
if [[ -n "$env_host" && -S "$env_host" ]]; then
if [[ "$env_host" == *podman* ]]; then
podman_sock="$env_host"
else
docker_sock="$env_host"
fi
fi
local pref
pref="$(echo "${ENGINE_PREF:-auto}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
case "$pref" in
docker|podman|auto) ;;
*)
warn "Unknown PEARDOCK_ENGINE='${ENGINE_PREF}', using auto"
pref=auto
;;
esac
local kind="" sock="" label=""
if [[ "$pref" == "docker" ]]; then
if [[ -n "$docker_sock" ]]; then
kind=docker; sock="$docker_sock"; label=Docker
else
die "PEARDOCK_ENGINE=docker but no Docker socket found. Install Docker (e.g. docker.io on Debian) and start it."
fi
elif [[ "$pref" == "podman" ]]; then
if [[ -z "$podman_sock" && "$has_podman_bin" -eq 1 ]]; then
try_enable_podman_system_socket || true
podman_sock="$(list_podman_sock_candidates | find_first_socket || true)"
fi
if [[ -n "$podman_sock" ]]; then
kind=podman; sock="$podman_sock"; label=Podman
elif [[ "$has_podman_bin" -eq 1 ]]; then
# Default path even if not up yet — configure for it
kind=podman
sock="/run/podman/podman.sock"
label=Podman
warn "Podman binary found but socket not up — will enable podman.socket and configure ${sock}"
else
die "PEARDOCK_ENGINE=podman but Podman is not installed. On Debian: sudo apt install podman"
fi
else
# auto: Docker first, then Podman (matches server runtime)
if [[ -n "$docker_sock" ]]; then
kind=docker; sock="$docker_sock"; label=Docker
elif [[ -n "$podman_sock" ]]; then
kind=podman; sock="$podman_sock"; label=Podman
elif [[ "$has_docker_bin" -eq 1 ]]; then
kind=docker; sock="/var/run/docker.sock"; label=Docker
warn "Docker CLI found but socket missing — install/start Docker and re-run if peardock cannot connect"
elif [[ "$has_podman_bin" -eq 1 ]]; then
kind=podman; sock="/run/podman/podman.sock"; label=Podman
warn "Podman CLI found but socket missing — enabling podman.socket"
try_enable_podman_system_socket || true
podman_sock="$(list_podman_sock_candidates | find_first_socket || true)"
[[ -n "$podman_sock" ]] && sock="$podman_sock"
else
warn "Neither Docker nor Podman socket detected."
if is_debian_family; then
warn "Debian/Ubuntu: sudo apt install docker.io OR sudo apt install podman"
warn " Docker: sudo systemctl enable --now docker"
warn " Podman: sudo systemctl enable --now podman.socket"
elif is_rhel_family; then
warn "RHEL/Fedora: install moby-engine/docker-ce or podman; enable docker or podman.socket"
else
warn "Install Docker or Podman, then re-run the installer or set PEARDOCK_ENGINE."
fi
# Default unit to docker group so a later Docker install works
kind=docker
sock="/var/run/docker.sock"
label=Docker
fi
fi
PEARDOCK_ENGINE_KIND="$kind"
PEARDOCK_ENGINE_SOCK="$sock"
PEARDOCK_ENGINE_LABEL="$label"
export PEARDOCK_ENGINE_KIND PEARDOCK_ENGINE_SOCK PEARDOCK_ENGINE_LABEL
log "Selected engine: ${C_BOLD}${label}${C_RESET} socket=${sock}"
if [[ -n "$docker_sock" ]]; then ok "Docker socket: ${docker_sock}"; else log "Docker socket: (none)"; fi
if [[ -n "$podman_sock" ]]; then ok "Podman socket: ${podman_sock}"; else log "Podman socket: (none)"; fi
}
# Full permission + group setup for the selected engine.
ensure_container_engine_access() {
detect_container_engines
local kind="$PEARDOCK_ENGINE_KIND"
local sock="$PEARDOCK_ENGINE_SOCK"
local label="$PEARDOCK_ENGINE_LABEL"
local grp
if [[ "$kind" == "podman" ]]; then
grp="podman"
# If socket already has a non-root group (e.g. some distros use podman), prefer it
if socket_exists "$sock"; then
local sg
sg="$(stat -c '%G' "$sock" 2>/dev/null || true)"
if [[ -n "$sg" && "$sg" != "root" ]]; then
grp="$sg"
fi
fi
try_enable_podman_system_socket || true
# Re-resolve after enable
local found
found="$(list_podman_sock_candidates | find_first_socket || true)"
if [[ -n "$found" ]]; then
sock="$found"
PEARDOCK_ENGINE_SOCK="$sock"
fi
configure_podman_socket_permissions "$sock" "$grp"
PEARDOCK_ENGINE_GROUP="$grp"
add_user_to_group peardock "$grp"
verify_socket_access "$sock" "$grp" "Podman" || true
else
grp="docker"
if socket_exists "$sock"; then
configure_docker_socket_permissions "$sock" "$grp"
grp="${PEARDOCK_ENGINE_GROUP:-docker}"
else
ensure_group_exists docker
PEARDOCK_ENGINE_GROUP=docker
grp=docker
fi
add_user_to_group peardock "$grp"
if socket_exists "$sock"; then
verify_socket_access "$sock" "$grp" "Docker" || true
else
warn "Docker socket not found at ${sock}"
if is_debian_family; then
warn "Debian/Ubuntu: sudo apt install docker.io && sudo systemctl enable --now docker"
warn "Then: sudo usermod -aG docker peardock && sudo systemctl restart peardock"
else
warn "Install/start Docker, then: sudo usermod -aG docker peardock && sudo systemctl restart peardock"
fi
fi
fi
# Export for unit file generation
PEARDOCK_DOCKER_GROUP="$grp"
export PEARDOCK_ENGINE_GROUP
# Back-compat for older variable name used in unit snippets
PEARDOCK_DOCKER_GROUP="${PEARDOCK_ENGINE_GROUP}"
export PEARDOCK_DOCKER_GROUP
write_engine_env "$kind" "$sock"
}
# ─── server install ─────────────────────────────────────────────────────────
@@ -403,36 +847,61 @@ install_server() {
run_root chmod 600 "${SERVER_DIR}/.env"
fi
# system user
# system user (Debian: nologin path + adduser --system; RHEL: useradd --system)
local shell_nologin
shell_nologin="$(nologin_shell)"
if ! id peardock >/dev/null 2>&1; then
log "Creating system user peardock…"
if command -v useradd >/dev/null 2>&1; then
run_root useradd --system --home "$SERVER_DIR" --shell /usr/sbin/nologin peardock 2>/dev/null \
if have_cmd useradd; then
run_root useradd --system --home-dir "$SERVER_DIR" --shell "$shell_nologin" --user-group peardock 2>/dev/null \
|| run_root useradd --system --home "$SERVER_DIR" --shell "$shell_nologin" peardock 2>/dev/null \
|| run_root useradd --system --home "$SERVER_DIR" --shell /bin/false peardock
elif command -v adduser >/dev/null 2>&1; then
run_root adduser --system --home "$SERVER_DIR" --shell /usr/sbin/nologin --no-create-home peardock || true
elif have_cmd adduser; then
# Debian/Ubuntu adduser
run_root adduser --system --group --home "$SERVER_DIR" --shell "$shell_nologin" --no-create-home peardock 2>/dev/null \
|| run_root adduser --system --home "$SERVER_DIR" --shell "$shell_nologin" --no-create-home peardock || true
else
die "Cannot create peardock user (need useradd or adduser)"
fi
fi
id peardock >/dev/null 2>&1 || die "peardock user was not created"
# Docker socket access: peardock must be in the docker group (mode 660 root:docker).
ensure_peardock_docker_group
# Docker and/or Podman: detect engine, groups, socket mode, .env PEARDOCK_ENGINE
ensure_container_engine_access
run_root chown -R peardock:peardock "$SERVER_DIR" 2>/dev/null || run_root chown -R peardock "$SERVER_DIR"
local eng_kind eng_sock eng_grp eng_label unit_after unit_wants read_paths
eng_kind="${PEARDOCK_ENGINE_KIND:-docker}"
eng_sock="${PEARDOCK_ENGINE_SOCK:-/var/run/docker.sock}"
eng_grp="${PEARDOCK_ENGINE_GROUP:-docker}"
eng_label="${PEARDOCK_ENGINE_LABEL:-Docker}"
unit_after="network-online.target"
unit_wants="network-online.target"
if [[ "$eng_kind" == "podman" ]]; then
unit_after="${unit_after} podman.socket"
unit_wants="${unit_wants} podman.socket"
else
unit_after="${unit_after} docker.service"
unit_wants="${unit_wants} docker.service"
fi
# Allow access to both common sockets so auto-fallback still works after install
read_paths="${SERVER_DIR} /var/run/docker.sock /run/docker.sock /run/podman/podman.sock /var/run/podman/podman.sock"
if [[ -n "$eng_sock" && "$read_paths" != *"$eng_sock"* ]]; then
read_paths="${read_paths} ${eng_sock}"
fi
# systemd unit
unit_path="/etc/systemd/system/peardock.service"
log "Writing ${unit_path}"
log "Writing ${unit_path} (engine=${eng_kind}, group=${eng_grp})"
run_root tee "$unit_path" >/dev/null <<EOF
[Unit]
Description=peardock HyperDHT Docker control plane
Description=peardock HyperDHT container control plane (${eng_label})
Documentation=${GITEA_URL}/${OWNER}/${REPO}
After=docker.service network-online.target
Wants=network-online.target
# docker.service may be missing on rootless setups — soft dependency
Wants=docker.service
After=${unit_after}
Wants=${unit_wants}
[Service]
Type=simple
@@ -443,21 +912,24 @@ RestartSec=5
TimeoutStartSec=30
TimeoutStopSec=30
User=peardock
# Primary + supplementary docker group so /var/run/docker.sock (root:docker 660) works
Group=${PEARDOCK_DOCKER_GROUP:-docker}
SupplementaryGroups=${PEARDOCK_DOCKER_GROUP:-docker}
# Socket group (docker or podman) so the Engine API socket is accessible
Group=${eng_grp}
SupplementaryGroups=${eng_grp}
NoNewPrivileges=true
PrivateTmp=true
Environment=NODE_ENV=production
Environment=PEARDOCK_ENGINE=${eng_kind}
Environment=DOCKER_HOST=unix://${eng_sock}
Environment=CONTAINER_HOST=unix://${eng_sock}
EnvironmentFile=-${SERVER_DIR}/.env
# Docker socket + state
ReadWritePaths=${SERVER_DIR} /var/run/docker.sock /run/docker.sock
# State + engine sockets (Docker and/or Podman)
ReadWritePaths=${read_paths}
[Install]
WantedBy=multi-user.target
EOF
if command -v systemctl >/dev/null 2>&1; then
if have_cmd systemctl; then
run_root systemctl daemon-reload
if confirm "Enable and start peardock.service now?" "y"; then
run_root systemctl enable --now peardock.service
@@ -485,6 +957,7 @@ ${C_BOLD}Server installed${C_RESET}
Binary: ${SERVER_DIR}/peardock-server
Config: ${SERVER_DIR}/.env ${C_BOLD}← check this file for your keys${C_RESET}
Service: peardock.service
Engine: ${eng_label} (${eng_kind}) socket=${eng_sock} group=${eng_grp}
${C_BOLD}Keys (written on first successful start)${C_RESET}
After peardock has started once, open the env file:
@@ -527,6 +1000,18 @@ ${C_BOLD}Connect from the desktop client → Add peer${C_RESET}
Docs: https://peardock.boats/docs/quickstart
EOF
if [[ "$eng_kind" == "podman" ]]; then
cat <<EOF
${C_BOLD}Podman notes${C_RESET}
· Rootful API: systemctl status podman.socket
· Socket group: ${eng_grp} (drop-in in /etc/systemd/system/podman.socket.d/)
· peardock is in group ${eng_grp}; unit sets PEARDOCK_ENGINE=podman
· Swarm is not available on Podman (UI hides it); use Pods / Stacks instead
· Prefer Registry Vault for private pulls (auth.json differs from Docker)
EOF
fi
}
# ─── client install ─────────────────────────────────────────────────────────
@@ -713,7 +1198,7 @@ main() {
if [[ -z "$ROLE" ]]; then
echo "What do you want to install?"
echo " ${C_BOLD}1${C_RESET}) Server — HyperDHT control plane (Docker host, Linux systemd)"
echo " ${C_BOLD}1${C_RESET}) Server — HyperDHT control plane (Docker or Podman host, Linux systemd)"
echo " ${C_BOLD}2${C_RESET}) Client — Desktop GUI (connect with a public key)"
echo " ${C_BOLD}3${C_RESET}) Both"
echo
@@ -727,6 +1212,28 @@ main() {
esac
fi
# Optional interactive engine pick for server installs when not forced
if [[ "$ROLE" == "server" || "$ROLE" == "both" ]]; then
local ep
ep="$(echo "${ENGINE_PREF}" | tr '[:upper:]' '[:lower:]')"
if [[ "$ASSUME_YES" != "1" && ( "$ep" == "auto" || -z "$ep" ) ]]; then
echo
echo "Container engine for the server:"
echo " ${C_BOLD}1${C_RESET}) Auto — Docker if present, else Podman (recommended)"
echo " ${C_BOLD}2${C_RESET}) Docker"
echo " ${C_BOLD}3${C_RESET}) Podman"
echo
local eng_choice
eng_choice="$(ask_choice "Choose 1/2/3" "1")"
case "$eng_choice" in
1|a|auto) ENGINE_PREF=auto ;;
2|d|docker) ENGINE_PREF=docker ;;
3|p|podman) ENGINE_PREF=podman ;;
*) ENGINE_PREF=auto ;;
esac
fi
fi
INSTALL_TMP="$(mktemp -d "${TMPDIR_ROOT}/peardock-install.XXXXXX")"
case "$ROLE" in