Adding a custom service beyond Puma + Sidekiq

Wiring a third long-running process (a sneakers consumer, Docker, anything else) into bosun's own format.

sudo bosun add <app-name> only ever generates two systemd units: puma-<app>.service and sidekiq-<app>.service (see systemd.md). Some apps need a third long-running process — a RabbitMQ consumer (sneakers), a different queue backend, a scheduled worker pool, whatever. This doc covers how to add one in bosun's own format, so it gets the same env loading, logging convention, and deploy-time start/stop handling every other bosun-managed process already gets, instead of a one-off hand-rolled unit that drifts from everything else on the box.

Standalone mode (no argus-tty): User=webapp-svc Group=webapps in the template below is actually User=<your account> Group=bosun-apps — match whatever /etc/bosun.conf/the puma/sidekiq units on your host already use. See standalone.md.

Why this isn't self-service the way install/setup are

Developers/admins have no whitelisted way to create or edit systemd unit files themselves (no sudo tee /etc/systemd/system/... in argus-tty's sudoers whitelist, by design — see the main README's "Who can do what"). bosun add's write_unit() is the only thing that creates unit files, and it only ever calls itself for puma/sidekiq. That leaves two paths for a third service:

Either way, your app's own deploy override (shared/do-release) is what actually starts/stops/restarts it on every deploy — see "Wiring it into deploy" below.

Path 1: a hand-created systemd unit

Ask an admin to create /etc/systemd/system/<name>-<app>.service, following exactly the same template bin/add.sh's write_unit() uses for puma/sidekiq — same user, same env loading, same restart policy:

[Unit]
  Description=<name> (<app>)
  After=network.target

  [Service]
  Type=simple
  User=webapp-svc
  Group=webapps
  WorkingDirectory=/var/www/<app>/current
  EnvironmentFile=-/var/www/<app>/shared/.env
  ExecStart=/var/www/<app>/shared/bin/<name>-exec
  Restart=on-failure
  RestartSec=3

  [Install]
  WantedBy=multi-user.target
sudo systemctl daemon-reload
  sudo systemctl enable <name>-<app>.service   # don't start yet — no exec wrapper exists until the next step

The exec wrapper (shared/bin/<name>-exec) — this part IS self-service

Unlike the unit file, shared/bin/ is webapps-group writable — any developer/admin can create this without an admin's help, same as bosun add already does for shared/bin/puma-exec/shared/bin/sidekiq-exec:

#!/bin/bash
  # shared/bin/<name>-exec — resolves RUBY_VERSION/RUBY_GEMSET from
  # deploy.conf at RUN time, not template time, same as puma-exec/
  # sidekiq-exec.
  set -euo pipefail
  set -a
  source "/var/www/<app>/shared/deploy.conf"
  [[ -f "/var/www/<app>/shared/.env" ]] && source "/var/www/<app>/shared/.env"
  set +a
  cd "/var/www/<app>/current"
  # RVM's wrapper binaries don't set rvm_path themselves — without it, RVM's
  # stock per-ruby .irbrc falls through to requiring a personal ~/irbrc that
  # usually doesn't exist, crashing irb/console for everyone.
  export rvm_path="/usr/local/rvm"
  # Own logfile via shell redirection (>> ... 2>&1) — NOT systemd's
  # StandardOutput=file:/StandardError=file:, and NOT a `-L`/`--logfile` CLI
  # flag if the process doesn't have one (Sidekiq 6.0+ removed its own
  # entirely). This is the one logging convention every bosun-managed
  # worker uses — see the sidekiq-exec/sneaker-exec wrappers for other apps
  # in this repo's scripts/ directory that already do this.
  exec "/usr/local/rvm/wrappers/${RUBY_VERSION}@${RUBY_GEMSET}/bundle" exec <your-command-here> \
    >> "/var/www/<app>/shared/log/<name>.log" 2>&1
chmod 755 /var/www/<app>/shared/bin/<name>-exec
  sudo systemctl start <name>-<app>.service
  tail -f /var/www/<app>/shared/log/<name>.log

Worked example: a sneakers (RabbitMQ) consumer

# unit: sneaker-myapp.service — ExecStart=/var/www/myapp/shared/bin/sneaker-exec
  # shared/bin/sneaker-exec's exec line:
  exec "/usr/local/rvm/wrappers/${RUBY_VERSION}@${RUBY_GEMSET}/bundle" exec rake sneakers:run \
    >> "/var/www/myapp/shared/log/sneaker.log" 2>&1

If the worker needs its own fixed config (which queue, concurrency) rather than a secret, bake it in as a plain export in the wrapper, same as -q/-c flags get baked into a sidekiq-<name>-exec wrapper — it isn't a secret, so it doesn't belong in shared/.env:

export WORKERS="GhaProcessQueueWorker"
  export RM_WORKER="4"

Path 2: Docker instead of systemd

No unit file, no admin needed — docker is already fully whitelisted:

sudo docker rm -f <name>-<app> 2>/dev/null || true
  sudo docker run -d --name <name>-<app> --restart unless-stopped \
    -v "/var/www/<app>/current:/app" -w /app --env-file "/var/www/<app>/shared/.env" \
    your-ruby-image bundle exec <your-command-here>

Your deploy override then calls docker stop|start|inspect <name>-<app> wherever the systemd path below calls systemctl/svc_active.

Wiring it into deploy

The built-in do-release only knows about puma-<app>/sidekiq-<app>. A third service needs a per-app override — copy do-release.sample to /var/www/<app>/shared/do-release, chmod +x it, and:

  1. Rename every *_worker function/variable to your service's name (e.g. WORKER_UNIT -> SNEAKER_UNIT, matching the unit name from Path 1, or the container name from Path 2).
  2. Swap the systemctl stop/start/svc_active calls in those renamed functions for docker stop/start/inspect if you went with Path 2.
  3. Deploy once with no other changes first, to confirm the override and the new service both actually come up, before customizing further.

See do-release.sample's own header comment for the exact lines to touch — it already documents this swap in detail. deployment/do-release (the built-in default) now also prints a one-line, non-blocking warning on every deploy for an app with no override at all, as a safety net against a forgotten or mistyped shared/do-release — see its own comment.

Logging — the one rule every custom service should follow

Own logfile via shell redirection in the exec wrapper (>> logfile 2>&1), not systemd's StandardOutput=file:/StandardError=file: and not a process's own -L/--logfile flag if it has one. Three reasons this is the standard across every bosun-managed worker: