Bus Services

Use Bus Services inside a Docker image.

Bus Services does not need to be PID 1. Install it in the image, run it from an entrypoint script, process supervisor, or docker exec, and let it manage the application processes inside the container. The practical requirements are a writable state directory, runtime secrets, an unprivileged user for native PostgreSQL, and a PID 1 that reaps child processes.

Runtime shape

  • Install Bus release binaries in the image, including bus, bus-services, and bus-integration-services.
  • Install the service binaries your stack uses, such as postgres, initdb, and bus-api-provider-events.
  • Copy services.yml to /opt/bus/services.yml.
  • Copy the profile tree to /opt/bus/profiles.
  • Mount /var/lib/bus and /var/log/bus as writable runtime locations.
  • Run Bus Services as an unprivileged user when native PostgreSQL is part of the stack. PostgreSQL refuses initdb and postgres as root.
  • Use a real init or supervisor such as tini, s6, or systemd when Bus Services is not PID 1. A bare sleep PID 1 does not reap stopped children.
  • Provide secrets such as BUS_EVENTS_JWT_SECRET at runtime, not in the image layer.

Start Services from inside the container

/usr/local/bin/bus services up \
  --file /opt/bus/services.yml \
  --profile-dir /opt/bus/profiles \
  --state-dir /var/lib/bus/services \
  --all

This command can run from an entrypoint script, a shell inside the container, or a supervisor-managed command. It exits after the stack is started and leaves the Services daemon plus child services running. --state-dir keeps frozen config, status files, token files, and logs out of the read-only stack location.

If the container exists only to host the Services stack, you can still add --foreground and make Bus Services the foreground child of tini. That is optional; it is not a PID 1 requirement.

Ubuntu image skeleton

FROM ubuntu:24.04

ARG BUS_RELEASE_TARBALL
ARG BUS_RELEASE_SHA256

RUN apt-get update \
  && apt-get install -y --no-install-recommends ca-certificates postgresql tini \
  && rm -rf /var/lib/apt/lists/*

COPY ${BUS_RELEASE_TARBALL} /tmp/busdk.tgz
RUN echo "${BUS_RELEASE_SHA256}  /tmp/busdk.tgz" | sha256sum -c - \
  && tar -xzf /tmp/busdk.tgz -C /usr/local/bin \
  && rm /tmp/busdk.tgz

COPY services.yml /opt/bus/services.yml
COPY profiles /opt/bus/profiles

RUN mkdir -p /var/lib/bus /var/log/bus \
  && chown -R postgres:postgres /var/lib/bus /var/log/bus /opt/bus

ENV PATH="/usr/local/bin:/usr/lib/postgresql/16/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
VOLUME ["/var/lib/bus", "/var/log/bus"]
USER postgres
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["sleep", "infinity"]

The CMD above is useful for development and smoke tests where you start Services with docker exec. Production images usually replace it with an entrypoint script or supervisor command that starts Bus Services, starts the main application, traps termination, and runs bus services down. Keep tini or another reaping PID 1 in front of that script.

PostgreSQL plus Events API

A single Ubuntu container can contain multiple Bus-managed services. This stack starts native PostgreSQL first, then starts an Events API provider that uses that PostgreSQL service as its backend.

version: "0"
env_files:
  - .env
services:
  postgres:
    profile: postgres-native
  events:
    profile: bus-events-api-provider-postgres
    depends_on: [postgres]
    params:
      port: 8081
BUS_POSTGRES_PORT=55432
BUS_POSTGRES_PGDATA=/var/lib/bus/postgres/data
BUS_POSTGRES_SOCKET_DIR=/tmp
BUS_EVENTS_JWT_SECRET=replace-at-runtime
BUS_EVENTS_POSTGRES_DSN=postgres://bus_service@127.0.0.1:55432/postgres?sslmode=disable

Events API profile

Profiles are the bridge between Services and the processes in your image. This profile starts the standalone Events API provider binary with the PostgreSQL backend and exposes a TCP health check on the configured port.

id: bus-events-api-provider-postgres
name: Bus Events API standalone PostgreSQL backend
service_kind: bus.api.events
parameters:
  - name: port
    type: integer
    description: Local TCP port for the Events API service.
    default: 8081
runtime:
  kind: native
  provider: bus
  command: [bus-api-provider-events]
  args: ["--addr", "127.0.0.1:{param:port}", "--events-backend", "postgres"]
  env:
    - name: PATH
      description: Process search path used to locate Bus and PostgreSQL binaries.
      value_from: process_env:PATH
    - name: BUS_EVENTS_JWT_SECRET
      description: HS256 secret used by the Events API to validate bearer tokens.
      value_from: env:BUS_EVENTS_JWT_SECRET
    - name: BUS_EVENTS_POSTGRES_DSN
      description: PostgreSQL connection string used as the durable Events backend.
      value_from: env:BUS_EVENTS_POSTGRES_DSN
    - name: BUS_EVENTS_API_PORT
      description: Local TCP port used by the Events API healthcheck.
      value: "{param:port}"
  healthcheck:
    tcp_port_from: env:BUS_EVENTS_API_PORT
    interval: 250ms
    timeout: 20s

Health check

/usr/local/bin/bus services list \
  --file /opt/bus/services.yml \
  --profile-dir /opt/bus/profiles \
  --state-dir /var/lib/bus/services \
  --format json

JSON status exits non-zero when the expected stack is degraded, while still writing the status payload. That gives Docker a real health signal and gives operators the same non-secret status output for troubleshooting. bus services ps remains a compatibility alias for existing Docker health checks and scripts.

Sidecars are optional

PostgreSQL and the Events API can run inside the same Ubuntu container when that is the deployment shape you need. They can also run as separate Compose services or platform-managed services. The important boundary is that the stack is explicit: Bus Services owns application service lifecycle, writable state is outside the read-only stack location, and connection details come from runtime configuration.