Skip to content

Deployment

The bridge is a single static binary with no inbound port. Run it anywhere that can reach the Screenshotter server privately and Slack over outbound HTTPS: on the server’s host, in the same private network, or on your tailnet.

Container

docker run --rm \
  -v "$PWD/config.toml:/app/config.toml:ro" \
  -e SLACK_BOT_TOKEN -e SLACK_APP_TOKEN \
  ghcr.io/mkende/screenshotter-slack-bridge:latest

The container needs only the config file and the two token variables. Make sure its egress allows *.slack.com and routes to the server.

Kubernetes

The bridge opens only outbound connections, so it needs no Service or Ingress: a single-replica Deployment with the config in a ConfigMap and the tokens in a Secret is the whole setup. Run it in a cluster that can reach the Screenshotter server, typically the same one.

apiVersion: v1
kind: ConfigMap
metadata:
  name: screenshotter-slack-bridge-config
data:
  config.toml: |
    # The address readers open when they click a card.
    screenshotter_base_url = "https://screen.corp.example"
    # The address the bridge itself fetches from: the server's in-cluster
    # Service. Drop this line when the two are the same.
    screenshotter_fetch_base_url = "http://screenshotter.default.svc"
    unfurl_domains = ["screen.corp.example"]
    bot_token_env_var = "SLACK_BOT_TOKEN"
    app_token_env_var = "SLACK_APP_TOKEN"
---
apiVersion: v1
kind: Secret
metadata:
  name: screenshotter-slack-bridge-tokens
stringData:
  SLACK_BOT_TOKEN: "xoxb-…"
  SLACK_APP_TOKEN: "xapp-…"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: screenshotter-slack-bridge
spec:
  replicas: 1
  selector:
    matchLabels:
      app: screenshotter-slack-bridge
  template:
    metadata:
      labels:
        app: screenshotter-slack-bridge
    spec:
      containers:
        - name: bridge
          image: ghcr.io/mkende/screenshotter-slack-bridge:latest
          args: ["-config", "/etc/screenshotter/config.toml"]
          envFrom:
            - secretRef:
                name: screenshotter-slack-bridge-tokens
          volumeMounts:
            - name: config
              mountPath: /etc/screenshotter
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: screenshotter-slack-bridge-config

One replica is enough. Slack spreads events across the Socket Mode connections of an app (up to ten), so more replicas only add capacity, for instance to avoid a gap during a rollout.

When the server runs in the same cluster, point screenshotter_fetch_base_url at its Service: that address is the bridge’s alone, and screenshotter_base_url keeps naming the address readers’ browsers resolve, which is what the card links to. The bridge sends no_redirect=1 on its fetches, so the server’s public canonical_address does not get in the way. If one address serves both purposes, set screenshotter_base_url only.

systemd

For a binary installed at /usr/local/bin/screenshotter-slack-bridge:

# /etc/systemd/system/screenshotter-slack-bridge.service
[Unit]
Description=Screenshotter Slack bridge
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/screenshotter-slack-bridge -config /etc/screenshotter/slack.toml
# Tokens come from the environment; keep this file mode 0600.
EnvironmentFile=/etc/screenshotter/slack.env
DynamicUser=yes
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now screenshotter-slack-bridge

/etc/screenshotter/slack.env holds one NAME=value line per token (SLACK_BOT_TOKEN=xoxb-… and SLACK_APP_TOKEN=xapp-…). -config defaults to ./config.toml.

Checking that it works

The bridge logs to stderr. A healthy start looks like:

screenshotter slack bridge 1.2.3 starting (unfurl domains: [screen.corp.example])
connecting to Slack via Socket Mode...
connected to Slack

connected to Slack means the connection is up and authenticated. Then paste a screenshot link in any channel: it should turn into a card within a few seconds. The bot does not need to be a member of the channel.

Before that, it is worth confirming from the bridge host that the server answers:

curl -sI 'http://screenshotter.internal:8080/<some-id>.png?no_redirect=1' | head -n 1

no_redirect=1 is what the bridge sends, so that a server configured with a public canonical_address serves the private request instead of redirecting it.

Troubleshooting

  • invalid Slack credentials, then exit. A token is wrong or the two are swapped: bot_token is the xoxb-… one, app_token the xapp-… one.
  • Loops on connecting…, never connected. Outbound network trouble, or the app-level token lacks connections:write, or Socket Mode is not enabled on the app.
  • Nothing unfurls. Check that the link host is in both the app’s unfurl domains and the bridge’s unfurl_domains, and that the message contains an explicit scheme (https://screen.corp.example/abcdef, not screen.corp.example/abcdef).
  • files.remote.add: missing_scope. Add the scope named in the log line under Bot Token Scopes, then reinstall the app.
  • files.remote.add: invalid_preview_image. The preview is too large for Slack; set or lower max_dimension.
  • The card appears empty and the image shows up seconds later. The card went out before Slack had processed the preview. Raise preview_wait, and check that the app has the remote_files:read scope.
  • Clicking the card goes nowhere. The card links to screenshotter_base_url, which the reader’s browser must be able to reach. Point it at an address readers can open if that is not the case.
  • image not found. The ID does not exist on the server; check the address the bridge fetches from (screenshotter_fetch_base_url, else screenshotter_base_url).
  • unexpected status 301 on the fetch. Something between the bridge and the server redirects the request, typically a reverse proxy forcing HTTPS. Point the bridge at the address that answers directly, with screenshotter_fetch_base_url if the card must keep linking elsewhere.
  • Some links are skipped under load (logged as “at capacity”). Raise max_concurrency, or lower max_image_workers if memory is the limit.