Explorar el Código

feat: run the webserver and runner in containers, and fix what that exposed

Adds a `runtime` target to the existing package pipeline and a compose file for
the two services on zeus. The database keeps its own compose file and lifecycle;
these only talk to it, through the host gateway, so bringing them up does not
mean recreating a running database.

Three pre-existing problems surfaced on the way, all of them the same shape -
something assuming one machine:

- Neither Dockerfile installed the MySQL or PostgreSQL client libraries, so the
  build quietly omitted the mysql-query and postgresql-query nodes behind a
  single STATUS line. Every .deb this pipeline has produced shipped without
  them, and a workflow using either fails with an unknown node type on those
  hosts and works on others.
- The .deb hard-Depends on smartbotic-database, which requires the daemon on the
  same machine as the services. Now Recommends.
- The runtime image deliberately does not install that .deb, for the same
  reason: it drags in a single-machine dependency chain when the database here
  is a separate container.

Two details in the image that are load-bearing:

- tzdata. Cron schedules resolve through std::chrono::locate_zone, so without
  the IANA database every zone falls back to UTC and a workflow set for 02:00
  Europe/Budapest fires at the wrong hour - silently, and only in the container.
- ADVERTISE_ADDRESS is set to the service name. Left unset, "localhost:9011"
  means the webserver's own container: the runner registers, reports online,
  and every dispatch goes nowhere.

The healthcheck is CMD + bash rather than CMD-SHELL. CMD-SHELL runs /bin/sh,
which is dash on Debian, and dash has no /dev/tcp - it reports a perfectly
healthy webserver as unhealthy, and the runner depends_on that health, so
nothing starts.
fszontagh hace 3 semanas
padre
commit
6edc60664b

+ 95 - 0
deploy/zeus/docker-compose.services.yml

@@ -0,0 +1,95 @@
+# The webserver and runner on zeus, in containers.
+#
+#   docker compose -f docker-compose.services.yml up -d
+#   docker compose -f docker-compose.services.yml logs -f
+#
+# The database is NOT here - it has its own compose file next to this one and
+# its own lifecycle. These two only talk to it.
+#
+# Build the image first (from the repo root):
+#   REPO_PASS=$(grep -oP 'RepoPass:\s*\K.*' /data/smartbotics-deb-repo/.env)
+#   docker buildx build -f packaging/Dockerfile.build \
+#     --build-arg BASE_IMAGE=smartbotic-automation-build-base:debian13 \
+#     --build-arg REPO_PASS="$REPO_PASS" \
+#     --target runtime -t smartbotic-automation:current .
+
+name: smartbotic
+
+services:
+  smartbotic-webserver:
+    image: smartbotic-automation:current
+    container_name: smartbotic-webserver
+    command: ["/usr/bin/smartbotic-webserver"]
+    restart: unless-stopped
+    networks: [smartbotic]
+    ports:
+      - "8090:8090"   # HTTP + WebUI
+      - "8091:8091"   # WebSocket, derived as http_port + 1
+    # The database container publishes 9004 on the host and sits on the default
+    # bridge. Rather than move it onto this network - which would mean
+    # recreating a running database - these services reach it back through the
+    # host gateway.
+    extra_hosts:
+      - "host.docker.internal:host-gateway"
+    environment:
+      TZ: Europe/Budapest
+      LOG_LEVEL: info
+      DATABASE_ADDRESS: host.docker.internal:9004
+      DATABASE_PROJECT: smartbotic-automation
+    volumes:
+      # Read-only: it carries the credentials master key and the JWT secret,
+      # and nothing should be rewriting it from inside a container.
+      - /data/dev/smartbotics/smartbotic/config:/var/lib/smartbotic/config:ro
+      # Bind-mounted rather than used from the image so a node can be edited and
+      # re-migrated without a rebuild, which is how they are worked on today.
+      - /data/dev/smartbotics/smartbotic/nodes:/usr/share/smartbotic-automation/nodes:ro
+    healthcheck:
+      # bash's /dev/tcp, because a slim image carries no curl or wget.
+      #
+      # It must be CMD + bash, NOT CMD-SHELL: CMD-SHELL runs /bin/sh, which on
+      # Debian is dash, and dash has no /dev/tcp - it fails with "cannot create
+      # /dev/tcp/...: Directory nonexistent" on a webserver that is serving
+      # perfectly well. The runner depends_on this being healthy, so getting it
+      # wrong stops the runner from ever starting.
+      test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8090"]
+      interval: 15s
+      timeout: 5s
+      retries: 5
+      start_period: 20s
+
+  smartbotic-runner:
+    image: smartbotic-automation:current
+    container_name: smartbotic-runner
+    command: ["/usr/bin/smartbotic-runner"]
+    restart: unless-stopped
+    networks: [smartbotic]
+    depends_on:
+      smartbotic-webserver:
+        condition: service_healthy
+    extra_hosts:
+      - "host.docker.internal:host-gateway"
+    environment:
+      TZ: Europe/Budapest
+      LOG_LEVEL: info
+      RUNNER_ID: runner-1
+      DATABASE_ADDRESS: host.docker.internal:9004
+      DATABASE_PROJECT: smartbotic-automation
+      # Service names, not localhost: the two are separate containers even when
+      # they share a host, so every one of these has to be stated.
+      WEBSERVER_ADDRESS: smartbotic-webserver:8090
+      NODE_SYNC_ADDRESS: smartbotic-webserver:9012
+      CREDENTIAL_SERVICE_ADDRESS: smartbotic-webserver:9013
+      # What the runner tells the webserver to reach it on. Left unset it says
+      # "localhost:9011", which inside a container means the webserver's own
+      # container - it registers, reports online, and every dispatch vanishes.
+      ADVERTISE_ADDRESS: smartbotic-runner:9011
+    volumes:
+      - /data/dev/smartbotics/smartbotic/config:/var/lib/smartbotic/config:ro
+      - /data/dev/smartbotics/smartbotic/nodes:/usr/share/smartbotic-automation/nodes:ro
+      # Written by imap-extract-attachments. A bind mount, so the 230 files
+      # carried over from mulan stay where the host can see them.
+      - /data/dev/smartbotics/smartbotic/data:/var/lib/smartbotic/data
+
+networks:
+  smartbotic:
+    name: smartbotic

+ 6 - 0
packaging/Dockerfile.base

@@ -39,6 +39,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
         nlohmann-json3-dev libspdlog-dev libfmt-dev \
         # curl / websockets
         libcurl4-openssl-dev libwebsockets-dev \
+        # MySQL/MariaDB + PostgreSQL clients. Without these the build still
+        # succeeds and simply omits the mysql-query and postgresql-query nodes,
+        # announced in one STATUS line - so a runner from this image registers a
+        # node list quietly missing them, and a workflow using either fails with
+        # an unknown node type only on hosts built this way.
+        libmariadb-dev libpq-dev \
         # WebUI build (Node.js + npm)
         nodejs npm \
     && rm -rf /var/lib/apt/lists/* \

+ 67 - 0
packaging/Dockerfile.build

@@ -32,6 +32,7 @@ RUN if [ ! -f /etc/smartbotic-automation-build-base ]; then \
             libssl-dev zlib1g-dev libbrotli-dev uuid-dev \
             protobuf-compiler libprotobuf-dev libgrpc++-dev protobuf-compiler-grpc \
             nlohmann-json3-dev libspdlog-dev libfmt-dev libcurl4-openssl-dev libwebsockets-dev \
+            libmariadb-dev libpq-dev \
             nodejs npm libsmartbotic-db-client-dev \
         && rm -rf /var/lib/apt/lists/*; \
     else echo "Using pre-built base: $(cat /etc/smartbotic-automation-build-base)"; fi
@@ -73,3 +74,69 @@ RUN chmod +x /build/packaging/scripts/create-deb.sh \
 # ----- Final export stage -----
 FROM scratch AS packages
 COPY --from=packager /packages/*.deb /
+
+# ----- Runtime image -----
+# A slim image that runs the services, as opposed to the `packages` stage which
+# only emits a .deb. Built by hand rather than by installing that .deb, because
+# the package pulls in a chain intended for a single-machine install; here the
+# database is a separate container and each service must be free to run on its
+# own host.
+#
+#   docker buildx build -f packaging/Dockerfile.build \
+#     --build-arg BASE_IMAGE=smartbotic-automation-build-base:debian13 \
+#     --build-arg REPO_PASS=<password> \
+#     --target runtime -t smartbotic-automation:current .
+FROM debian:trixie-slim AS runtime
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+ARG REPO_USER=callerai
+ARG REPO_PASS
+COPY packaging/smartbotics-repo.gpg /usr/share/keyrings/smartbotics-repo.gpg
+
+# libsmartbotic-db-client comes from the SmartBotics repo; everything else is
+# Debian. The credential is written and removed inside one layer so it is not
+# left in the image.
+RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
+    && echo "deb [signed-by=/usr/share/keyrings/smartbotics-repo.gpg] https://repository.smartbotics.ai trixie main" \
+        > /etc/apt/sources.list.d/smartbotics.list \
+    && printf "machine repository.smartbotics.ai\nlogin %s\npassword %s\n" \
+        "$REPO_USER" "$REPO_PASS" > /etc/apt/auth.conf.d/smartbotics.conf \
+    && chmod 600 /etc/apt/auth.conf.d/smartbotics.conf \
+    && apt-get update \
+    && apt-get install -y --no-install-recommends \
+        libsmartbotic-db-client \
+        libssl3t64 libprotobuf32t64 libgrpc++1.51t64 libspdlog1.15 libfmt10 \
+        libuuid1 libcurl4t64 zlib1g libbrotli1 libwebsockets19t64 \
+        libmariadb3 libpq5 \
+        tzdata \
+    && rm -f /etc/apt/auth.conf.d/smartbotics.conf /etc/apt/sources.list.d/smartbotics.list \
+    && rm -rf /var/lib/apt/lists/*
+
+# Timezone data matters here: cron schedules are evaluated in the workflow's own
+# zone through std::chrono::locate_zone, which needs the IANA database present.
+# Without tzdata every zone falls back to UTC and a workflow set for 02:00
+# Europe/Budapest fires at the wrong hour, silently.
+
+RUN useradd --system --create-home --home-dir /var/lib/smartbotic --shell /usr/sbin/nologin smartbotic
+
+COPY --from=builder /build/build/smartbotic-webserver /usr/bin/
+COPY --from=builder /build/build/smartbotic-runner    /usr/bin/
+COPY --from=builder /build/webui/dist/ /usr/share/smartbotic-automation/webui/
+COPY --from=builder /build/nodes/      /usr/share/smartbotic-automation/nodes/
+COPY --from=builder /build/config/     /usr/share/smartbotic-automation/config/
+
+# The working directory is what config/ and data/ resolve against.
+WORKDIR /var/lib/smartbotic
+RUN mkdir -p /var/lib/smartbotic/data /var/lib/smartbotic/config \
+    && cp -r /usr/share/smartbotic-automation/config/. /var/lib/smartbotic/config/ \
+    && ln -s /usr/share/smartbotic-automation/nodes /var/lib/smartbotic/nodes \
+    && chown -R smartbotic:smartbotic /var/lib/smartbotic
+
+USER smartbotic
+ENV WEBUI_PATH=/usr/share/smartbotic-automation/webui \
+    NODES_PATH=/usr/share/smartbotic-automation/nodes \
+    LOG_LEVEL=info
+
+# No default CMD: compose names the service to run, because one image serves
+# both and guessing here would start the wrong one.

+ 2 - 1
packaging/deb/templates/control.smartbotic-automation

@@ -12,5 +12,6 @@ Homepage: https://smartbotics.ai
 Section: contrib/utils
 Priority: optional
 License: proprietary
-Depends: smartbotic-database (>= 2.3.1), libsmartbotic-db-client (>= 2.3.1), libc6 (>= 2.17), adduser, libssl3t64, libprotobuf32t64, libgrpc++1.51t64, libspdlog1.15, libfmt10, libuuid1, libcurl4t64, zlib1g, libbrotli1, libwebsockets19t64, nodejs
+Depends: libsmartbotic-db-client (>= 2.3.1), libc6 (>= 2.17), adduser, libssl3t64, libprotobuf32t64, libgrpc++1.51t64, libspdlog1.15, libfmt10, libuuid1, libcurl4t64, zlib1g, libbrotli1, libwebsockets19t64, libmariadb3, libpq5, nodejs
+Recommends: smartbotic-database (>= 2.3.1)
 Installed-Size: {{INSTALLED_SIZE}}