#!/bin/sh # nomploy installer — sets up the nomploy control plane on a fresh Linux server. # # curl -sSL https://nomploy.com/install.sh | sh # # Installs: Docker, Consul, Nomad, CNI plugins, then runs Postgres, Redis, # Traefik and the nomploy app. Idempotent — safe to re-run. # # nomploy is a fork of Dokploy (Apache-2.0) that uses HashiCorp Nomad as the # orchestrator. See https://github.com/Nomploy/nomploy. set -e NOMPLOY_IMAGE="${NOMPLOY_IMAGE:-ghcr.io/nomploy/nomploy:latest}" NOMPLOY_PORT="${NOMPLOY_PORT:-3000}" CNI_VERSION="${CNI_VERSION:-v1.5.1}" CONSUL_CNI_VERSION="${CONSUL_CNI_VERSION:-1.6.3}" # ── privilege + platform detection ───────────────────────────────────────── if [ "$(id -u)" -eq 0 ]; then SUDO="" else if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then SUDO="sudo" else echo "Error: run as root or with passwordless sudo." >&2 exit 1 fi fi if [ ! -f /etc/os-release ]; then echo "Error: unsupported OS (no /etc/os-release)." >&2 exit 1 fi OS_TYPE="$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')" ARCH="$(uname -m)" case "$ARCH" in x86_64) CNI_ARCH=amd64 ;; aarch64 | arm64) CNI_ARCH=arm64 ;; *) echo "Error: unsupported architecture $ARCH." >&2; exit 1 ;; esac echo "==> Installing nomploy on ${OS_TYPE} (${ARCH})" # ── Docker ────────────────────────────────────────────────────────────────── if ! command -v docker >/dev/null 2>&1; then echo "==> Installing Docker" curl -fsSL https://get.docker.com | $SUDO sh $SUDO systemctl enable --now docker else echo "Docker already installed." fi # ── Consul + Nomad (HashiCorp repos) ───────────────────────────────────────── install_hashicorp_debian() { export DEBIAN_FRONTEND=noninteractive $SUDO apt-get update -y $SUDO apt-get install -y curl gnupg lsb-release curl -fsSL https://apt.releases.hashicorp.com/gpg \ | $SUDO gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" \ | $SUDO tee /etc/apt/sources.list.d/hashicorp.list >/dev/null $SUDO apt-get update -y $SUDO apt-get install -y nomad consul } install_hashicorp_rhel() { $SUDO yum install -y yum-utils $SUDO yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo $SUDO yum -y install nomad consul } if ! command -v nomad >/dev/null 2>&1 || ! command -v consul >/dev/null 2>&1; then echo "==> Installing Consul + Nomad" case "$OS_TYPE" in ubuntu | debian | raspbian | pop | linuxmint | zorin) install_hashicorp_debian ;; centos | rhel | rocky | almalinux | fedora | amzn | ol) install_hashicorp_rhel ;; *) echo "Error: unsupported OS $OS_TYPE for auto-install." >&2; exit 1 ;; esac else echo "Consul + Nomad already installed." fi # ── CNI plugins (Nomad bridge networking) ──────────────────────────────────── if [ ! -f /opt/cni/bin/bridge ]; then echo "==> Installing CNI plugins" $SUDO mkdir -p /opt/cni/bin curl -fsSL "https://github.com/containernetworking/plugins/releases/download/${CNI_VERSION}/cni-plugins-linux-${CNI_ARCH}-${CNI_VERSION}.tgz" \ | $SUDO tar -C /opt/cni/bin -xz fi # consul-cni: required by Consul Connect transparent proxy (Phase B). It writes # the iptables redirect inside each mesh alloc so the app keeps using normal # service names while the Envoy sidecar transparently enforces intentions. if [ ! -f /opt/cni/bin/consul-cni ]; then echo "==> Installing consul-cni" $SUDO mkdir -p /opt/cni/bin tmpz="$(mktemp)" if curl -fsSL "https://releases.hashicorp.com/consul-cni/${CONSUL_CNI_VERSION}/consul-cni_${CONSUL_CNI_VERSION}_linux_${CNI_ARCH}.zip" -o "$tmpz"; then $SUDO unzip -o "$tmpz" -d /opt/cni/bin >/dev/null 2>&1 || \ { command -v unzip >/dev/null 2>&1 || { $SUDO apt-get install -y unzip >/dev/null 2>&1 || true; }; $SUDO unzip -o "$tmpz" -d /opt/cni/bin >/dev/null 2>&1; } fi rm -f "$tmpz" fi # Bridge networking for Nomad `mode="bridge"` allocs. Load br_netfilter (creates # the bridge-nf-call sysctls) and PERSIST both the module and the sysctls, so a # reboot doesn't drop bridge networking and leave bridge-mode jobs unplaceable # ("missing network"). Without persistence, a host reboot silently breaks apps. $SUDO modprobe bridge >/dev/null 2>&1 || true $SUDO modprobe br_netfilter >/dev/null 2>&1 || true printf 'bridge\nbr_netfilter\n' | $SUDO tee /etc/modules-load.d/nomploy-bridge.conf >/dev/null 2>&1 || true printf 'net.bridge.bridge-nf-call-iptables = 1\nnet.bridge.bridge-nf-call-ip6tables = 1\nnet.ipv4.ip_forward = 1\n' \ | $SUDO tee /etc/sysctl.d/99-nomploy-bridge.conf >/dev/null 2>&1 || true $SUDO sysctl --system >/dev/null 2>&1 || true # ── Consul + Nomad config (single node: server + client) ───────────────────── $SUDO mkdir -p /etc/consul.d /opt/consul /etc/nomad.d /opt/nomad # ── WireGuard hub (cluster overlay 10.10.0.0/24) ───────────────────────────── # The control plane is the WireGuard hub; Consul/Nomad servers bind to the hub's # overlay IP so worker nodes can join over an encrypted mesh. Consul/Nomad HTTP # APIs still answer on 127.0.0.1 for the local app. Endpoint workers dial defaults # to this host's public IP (open UDP 51820); override with NOMPLOY_WG_ENDPOINT. $SUDO mkdir -p /etc/nomploy if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get install -y wireguard wireguard-tools >/dev/null 2>&1 || true else $SUDO yum install -y wireguard-tools >/dev/null 2>&1 || true fi $SUDO mkdir -p /etc/wireguard && $SUDO chmod 700 /etc/wireguard if [ ! -s /etc/wireguard/hub_priv ]; then $SUDO sh -c 'wg genkey > /etc/wireguard/hub_priv && chmod 600 /etc/wireguard/hub_priv && wg pubkey < /etc/wireguard/hub_priv > /etc/wireguard/hub_pub' fi HUB_PUB="$($SUDO cat /etc/wireguard/hub_pub)" if [ ! -f /etc/wireguard/wg0.conf ]; then $SUDO tee /etc/wireguard/wg0.conf >/dev/null </dev/null 2>&1 || $SUDO wg-quick up wg0 || true $SUDO systemctl enable wg-quick@wg0 >/dev/null 2>&1 || true # Shared gossip encryption key (Consul + Nomad). [ -s /etc/nomploy/gossip.key ] || consul keygen | $SUDO tee /etc/nomploy/gossip.key >/dev/null GOSSIP="$($SUDO cat /etc/nomploy/gossip.key)" WG_ENDPOINT="${NOMPLOY_WG_ENDPOINT:-$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}'):51820}" $SUDO tee /etc/consul.d/consul.hcl >/dev/null </dev/null </dev/null </dev/null # Registry auth via consul-template: render /root/.docker/config.json from Consul # KV (nomploy/docker-auth) so private-registry pulls work cluster-wide with no # credentials in job specs. Keep in sync with setup/registry-auth.ts. if ! command -v consul-template >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get install -y consul-template 2>&1 || true; elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y consul-template 2>&1 || true; fi fi $SUDO mkdir -p /etc/consul-template.d printf '%s\n' '{{ keyOrDefault "nomploy/docker-auth" "{}" }}' | $SUDO tee /etc/consul-template.d/docker-auth.tpl >/dev/null $SUDO tee /etc/consul-template.d/docker-auth.hcl >/dev/null <<'CT' consul { address = "127.0.0.1:8500" } template { source = "/etc/consul-template.d/docker-auth.tpl" destination = "/root/.docker/config.json" perms = "0600" } CT $SUDO tee /etc/systemd/system/nomploy-registry-auth.service >/dev/null <<'UNIT' [Unit] Description=nomploy registry auth (consul-template renders docker config) After=consul.service network-online.target [Service] ExecStart=/usr/bin/consul-template -config /etc/consul-template.d/docker-auth.hcl Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target UNIT $SUDO systemctl daemon-reload 2>/dev/null || true $SUDO systemctl enable --now nomploy-registry-auth.service 2>&1 || true # Start via --no-block and poll the HTTP APIs for readiness. The packaged units # are Type=notify; if the agent doesn't signal systemd, a blocking `restart` # would hang and (under set -e) abort the install even though the agent is up. wait_api() { name="$1"; url="$2" echo "==> Waiting for $name API" i=0 while [ "$i" -lt 60 ]; do if curl -fsS "$url" >/dev/null 2>&1; then echo "$name is up." return 0 fi i=$((i + 1)) sleep 2 done echo "Error: $name did not become ready in time." >&2 return 1 } echo "==> Starting Consul + Nomad" # The packaged consul unit is Type=notify; on some builds the readiness signal # never arrives, so systemd kills it at TimeoutStartSec and restart-loops. Track # it by process liveness instead. $SUDO mkdir -p /etc/systemd/system/consul.service.d /etc/systemd/system/nomad.service.d $SUDO tee /etc/systemd/system/consul.service.d/type.conf >/dev/null <<'CONSULUNIT' [Service] Type=exec CONSULUNIT # Consul binds to the WireGuard overlay IP, so it must not start before wg0 is up # — otherwise it can't bind and the cluster fails to form quorum after a reboot. # Nomad advertises over wg and depends on Consul. Order both after wg-quick@wg0 # (and Nomad after Consul), with a short wait-for-wg0 guard. $SUDO tee /etc/systemd/system/consul.service.d/10-nomploy-wg.conf >/dev/null <<'CONSULWG' [Unit] After=wg-quick@wg0.service Wants=wg-quick@wg0.service [Service] RestartSec=3 ExecStartPre=/bin/sh -c "for i in $(seq 1 60); do ip -4 addr show wg0 2>/dev/null | grep -q 'inet ' && exit 0; sleep 1; done; exit 0" CONSULWG $SUDO tee /etc/systemd/system/nomad.service.d/10-nomploy-wg.conf >/dev/null <<'NOMADWG' [Unit] After=wg-quick@wg0.service consul.service Wants=wg-quick@wg0.service [Service] RestartSec=3 ExecStartPre=/bin/sh -c "for i in $(seq 1 60); do ip -4 addr show wg0 2>/dev/null | grep -q 'inet ' && exit 0; sleep 1; done; exit 0" NOMADWG $SUDO systemctl daemon-reload $SUDO systemctl enable consul nomad >/dev/null 2>&1 || true $SUDO systemctl restart --no-block consul wait_api Consul "http://127.0.0.1:8500/v1/status/leader" # ── ACL bootstrap (Consul) ────────────────────────────────────────────────── # Consul is up with default_policy=deny. Bootstrap the ACL system and mint one # least-privilege token per consumer, persisted under /etc/nomploy/secrets (0700). # The panel bind-mounts /etc/nomploy, so it reads these to authenticate to # Consul/Nomad AND to hand joining nodes their agent tokens. Idempotent: a rerun # reuses the stored tokens (bootstrap itself can run only once). SECRETS=/etc/nomploy/secrets $SUDO mkdir -p "$SECRETS"; $SUDO chmod 700 "$SECRETS" if [ ! -s "$SECRETS/consul-mgmt.token" ]; then echo "==> Bootstrapping Consul ACLs" i=0; BOOT="" while [ "$i" -lt 30 ]; do BOOT="$(consul acl bootstrap 2>/dev/null)" && [ -n "$BOOT" ] && break i=$((i + 1)); sleep 2 done echo "$BOOT" | awk '/SecretID/{print $2}' | $SUDO tee "$SECRETS/consul-mgmt.token" >/dev/null fi export CONSUL_HTTP_TOKEN="$($SUDO cat "$SECRETS/consul-mgmt.token")" mint_consul_policy() { # name rules consul acl policy read -name "$1" >/dev/null 2>&1 || \ consul acl policy create -name "$1" -rules "$2" >/dev/null 2>&1 || true } mint_consul_token() { # file policy-name (reuse if the file already exists) if [ ! -s "$SECRETS/$1" ]; then consul acl token create -policy-name "$2" 2>/dev/null \ | awk '/SecretID/{print $2}' | $SUDO tee "$SECRETS/$1" >/dev/null fi $SUDO cat "$SECRETS/$1" } # Policy rules mirror the live cluster exactly. mint_consul_policy nomploy-agent 'node_prefix "" { policy = "write" } service_prefix "" { policy = "read" } agent_prefix "" { policy = "write" } session_prefix "" { policy = "write" }' mint_consul_policy nomploy-readonly 'node_prefix "" { policy = "read" } service_prefix "" { policy = "read" }' mint_consul_policy nomploy-ct 'key_prefix "nomploy/docker-auth" { policy = "read" }' mint_consul_policy nomploy-nomad 'service_prefix "" { policy = "write" } key_prefix "" { policy = "read" } node_prefix "" { policy = "read" } agent_prefix "" { policy = "read" }' C_AGENT="$(mint_consul_token consul-agent.token nomploy-agent)" C_RO="$(mint_consul_token consul-readonly.token nomploy-readonly)" C_CT="$(mint_consul_token consul-ct.token nomploy-ct)" C_NOMAD="$(mint_consul_token consul-nomad.token nomploy-nomad)" C_TRAEFIK="$(mint_consul_token consul-traefik.token nomploy-readonly)" # Panel: management — it reads the catalog/KV, writes intentions + the registry KV, # and mints node tokens on join. C_PANEL="$(mint_consul_token consul-panel.token global-management)" # The agent's own token (anti-entropy/registration) + the DEFAULT token that # governs tokenless requests and DNS. Default is read-only, so DNS resolves but a # tokenless caller can neither write nor control agents. consul acl set-agent-token agent "$C_AGENT" >/dev/null 2>&1 || true consul acl set-agent-token default "$C_RO" >/dev/null 2>&1 || true # Give registry-auth consul-template its token so it can read the KV under deny. $SUDO tee /etc/consul-template.d/docker-auth.hcl >/dev/null <&1 || true # Inject the Nomad→Consul integration token before Nomad starts. $SUDO sed -i "s|__NOMAD_CONSUL_TOKEN__|$C_NOMAD|" /etc/nomad.d/nomad.hcl $SUDO systemctl restart --no-block nomad wait_api Nomad "http://127.0.0.1:4646/v1/agent/health" # ── ACL bootstrap (Nomad) ─────────────────────────────────────────────────── if [ ! -s "$SECRETS/nomad-mgmt.token" ]; then echo "==> Bootstrapping Nomad ACLs" i=0; NBOOT="" while [ "$i" -lt 30 ]; do NBOOT="$(nomad acl bootstrap 2>/dev/null)" && [ -n "$NBOOT" ] && break i=$((i + 1)); sleep 2 done echo "$NBOOT" | awk '/Secret ID/{print $4}' | $SUDO tee "$SECRETS/nomad-mgmt.token" >/dev/null fi export NOMAD_TOKEN="$($SUDO cat "$SECRETS/nomad-mgmt.token")" # Panel token = management (deploy/scale/drain/exec/logs + mint node tokens on join). if [ ! -s "$SECRETS/nomad-panel.token" ]; then nomad acl token create -type=management -name=nomploy-panel 2>/dev/null \ | awk '/Secret ID/{print $4}' | $SUDO tee "$SECRETS/nomad-panel.token" >/dev/null fi N_PANEL="$($SUDO cat "$SECRETS/nomad-panel.token")" # Autoscaler token = least-privilege (scale jobs + drain nodes for cluster scaling). if ! nomad acl policy info nomad-autoscaler >/dev/null 2>&1; then $SUDO tee /tmp/nomploy-autoscaler.policy.hcl >/dev/null <<'ASP' namespace "*" { policy = "read" capabilities = ["scale-job", "read-job", "list-jobs", "read-logs"] } node { policy = "write" } operator { policy = "read" } ASP nomad acl policy apply -description "nomad-autoscaler least-priv" nomad-autoscaler /tmp/nomploy-autoscaler.policy.hcl >/dev/null 2>&1 || true $SUDO rm -f /tmp/nomploy-autoscaler.policy.hcl fi if [ ! -s "$SECRETS/nomad-autoscaler.token" ]; then nomad acl token create -name=nomad-autoscaler -policy=nomad-autoscaler 2>/dev/null \ | awk '/Secret ID/{print $4}' | $SUDO tee "$SECRETS/nomad-autoscaler.token" >/dev/null fi N_AS="$($SUDO cat "$SECRETS/nomad-autoscaler.token")" $SUDO chmod 600 "$SECRETS"/*.token 2>/dev/null || true # ── Cluster DNS (dnsmasq) ─────────────────────────────────────────────────── # Nomad allocations point their DNS at the hub's WireGuard IP so they can resolve # each other and their databases by name. dnsmasq (running as root, so it can bind # :53) forwards *.service.consul to the local Consul DNS and everything else to a # public resolver. echo "==> Configuring cluster DNS (dnsmasq)" $SUDO apt-get install -y dnsmasq >/dev/null 2>&1 || $SUDO yum -y install dnsmasq >/dev/null 2>&1 || true $SUDO tee /etc/dnsmasq.d/nomploy-consul.conf >/dev/null <<'DNSMASQ' bind-interfaces listen-address=10.10.0.1 port=53 no-resolv server=/consul/127.0.0.1#8600 server=1.1.1.1 server=8.8.8.8 DNSMASQ # dnsmasq binds listen-address=10.10.0.1 (the wg IP), so it must start after wg0 is # up — otherwise a reboot leaves it dead ("Cannot assign requested address") and the # node's Cluster DNS resolver fails (breaking .consul name resolution). $SUDO mkdir -p /etc/systemd/system/dnsmasq.service.d $SUDO tee /etc/systemd/system/dnsmasq.service.d/10-nomploy-wg.conf >/dev/null <<'DNSMASQWG' [Unit] After=wg-quick@wg0.service Wants=wg-quick@wg0.service [Service] RestartSec=3 ExecStartPre=/bin/sh -c "for i in $(seq 1 60); do ip -4 addr show wg0 2>/dev/null | grep -q 'inet ' && exit 0; sleep 1; done; exit 0" DNSMASQWG $SUDO systemctl daemon-reload $SUDO systemctl enable dnsmasq >/dev/null 2>&1 || true $SUDO systemctl restart dnsmasq >/dev/null 2>&1 || true # ── Datastores ──────────────────────────────────────────────────────────────── POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-amukds4wi9001583845717ad2}" run_container() { name="$1"; shift if [ "$($SUDO docker ps -q -f name="^${name}$" -f status=running)" ]; then echo "${name} already running." return fi $SUDO docker rm -f "$name" >/dev/null 2>&1 || true $SUDO docker run -d --name "$name" --restart unless-stopped "$@" } echo "==> Starting Postgres + Redis" run_container nomploy-postgres \ -e POSTGRES_USER=nomploy -e POSTGRES_DB=nomploy \ -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \ -v nomploy-postgres:/var/lib/postgresql/data \ -p 127.0.0.1:5432:5432 postgres:16 run_container nomploy-redis \ -v nomploy-redis:/data -p 127.0.0.1:6379:6379 redis:7 # ── Traefik (ingress via Consul Catalog) ───────────────────────────────────── # Discovers deployed Nomad services from Consul (each carries traefik.* tags) and # routes HTTP/HTTPS to them. Mirrors the app's initializeTraefikNomad(). TRAEFIK_DIR="/etc/nomploy/traefik" $SUDO mkdir -p "$TRAEFIK_DIR/dynamic" $SUDO tee "$TRAEFIK_DIR/traefik.yml" >/dev/null <<'TRAEFIKYML' entryPoints: web: address: ":80" websecure: address: ":443" providers: consulCatalog: endpoint: address: "http://127.0.0.1:8500" exposedByDefault: false prefix: traefik # File provider: nomploy writes dynamic routes here (e.g. the panel's own # domain). Without this, setting a domain in the UI has no effect. file: directory: "/etc/nomploy/traefik/dynamic" watch: true api: insecure: true dashboard: true certificatesResolvers: letsencrypt: acme: email: admin@localhost storage: /etc/traefik/acme.json httpChallenge: entryPoint: web TRAEFIKYML # Traefik reads the Consul catalog under ACLs — give its provider the read-only # token (service:read + node:read) so it can still discover routes. $SUDO sed -i "s|address: \"http://127.0.0.1:8500\"|address: \"http://127.0.0.1:8500\"\n token: \"$C_TRAEFIK\"|" "$TRAEFIK_DIR/traefik.yml" if [ ! -f "$TRAEFIK_DIR/acme.json" ]; then $SUDO touch "$TRAEFIK_DIR/acme.json" $SUDO chmod 600 "$TRAEFIK_DIR/acme.json" fi echo "==> Starting Traefik" run_container nomploy-traefik \ --network host \ --add-host nomploy:127.0.0.1 \ -v "$TRAEFIK_DIR/traefik.yml:/etc/traefik/traefik.yml:ro" \ -v "$TRAEFIK_DIR/acme.json:/etc/traefik/acme.json" \ -v "$TRAEFIK_DIR/dynamic:/etc/nomploy/traefik/dynamic" \ traefik:v3.0 # ── Nomad Autoscaler ───────────────────────────────────────────────────────── # Reads scaling{} policies from Nomad jobs (emitted from compose x-nomad-scaling) # and scales task groups via the nomad-apm metrics source. echo "==> Starting Nomad Autoscaler" run_container nomad-autoscaler \ --network host \ -e NOMAD_TOKEN="$N_AS" \ hashicorp/nomad-autoscaler:latest \ agent -nomad-address=http://127.0.0.1:4646 -http-bind-address=127.0.0.1 -http-bind-port=8081 # Stable auth secret: generate once and persist, so sessions survive restarts # (and we don't fall back to the insecure hardcoded default). $SUDO mkdir -p /etc/nomploy AUTH_SECRET_FILE=/etc/nomploy/auth-secret if [ ! -s "$AUTH_SECRET_FILE" ]; then (openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n') \ | $SUDO tee "$AUTH_SECRET_FILE" >/dev/null $SUDO chmod 600 "$AUTH_SECRET_FILE" fi BETTER_AUTH_SECRET="$($SUDO cat "$AUTH_SECRET_FILE")" # ── nomploy app (Nomad job) ─────────────────────────────────────────────────── # The panel runs as a Nomad job named "nomploy" so it can self-update the Dokploy # way: the UI's Reload/Update re-submits this job with a new image (nomad job # run), which pulls it and rolling-restarts the allocation in place. `privileged` # + host network + the three bind mounts reproduce exactly what the old # `docker run` bootstrap gave it; `allow_privileged = true` is already set on the # Nomad docker plugin, so no cluster config change is needed. Keep this HCL in # sync with packages/server/src/utils/builders/nomad-panel.ts (the self-update # path renders the same job). echo "==> Starting nomploy ($NOMPLOY_IMAGE)" $SUDO docker pull "$NOMPLOY_IMAGE" # Drop any pre-Nomad panel container so it can't fight the job for port 3000. $SUDO docker rm -f nomploy >/dev/null 2>&1 || true # Nomad was started with --no-block above; wait for its API before submitting. echo "==> Waiting for Nomad API" for _ in $(seq 1 60); do curl -fsS http://127.0.0.1:4646/v1/status/leader >/dev/null 2>&1 && break sleep 2 done # Let allocations burst above their memory reservation up to memory_max, using # free host RAM (best-effort, like the old `docker run` panel). The panel job # relies on this to reach memory_max=2048 without reserving it all up front — # important on small control-plane nodes. $SUDO nomad operator scheduler set-config -memory-oversubscription=true >/dev/null 2>&1 || true # Swap safety-valve on the control-plane node. A rolling panel deploy runs two # panels at once (old + canary, up to memory_max=2048 each) beside Postgres on a # small host — without swap that overlap starves Postgres into transient connect # timeouts, crash-looping the new panel (downtime). A 2G swapfile (low swappiness # so RAM is still preferred) absorbs the spike. Idempotent. if ! $SUDO swapon --show 2>/dev/null | grep -q /swapfile; then $SUDO dd if=/dev/zero of=/swapfile bs=1M count=2048 status=none 2>/dev/null && $SUDO chmod 600 /swapfile && $SUDO mkswap /swapfile >/dev/null 2>&1 && $SUDO swapon /swapfile 2>/dev/null || true grep -q "/swapfile" /etc/fstab 2>/dev/null || echo "/swapfile none swap sw 0 0" | $SUDO tee -a /etc/fstab >/dev/null fi $SUDO sysctl -w vm.swappiness=10 >/dev/null 2>&1 || true echo "vm.swappiness=10" | $SUDO tee /etc/sysctl.d/99-nomploy.conf >/dev/null 2>&1 || true $SUDO mkdir -p /etc/nomploy $SUDO tee /etc/nomploy/nomploy.nomad.hcl >/dev/null <}:${NOMPLOY_PORT}" echo " Ingress: Traefik on :80 / :443 (dashboard :8080)" echo " Nomad: http://${IP:-}:4646" echo " Consul: http://${IP:-}:8500" echo "=============================================="