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:latestThe 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-configOne 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.targetsudo 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 Slackconnected 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 1no_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_tokenis thexoxb-…one,app_tokenthexapp-…one.- Loops on
connecting…, neverconnected. Outbound network trouble, or the app-level token lacksconnections: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, notscreen.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 lowermax_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 theremote_files:readscope. - 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, elsescreenshotter_base_url).unexpected status 301on 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, withscreenshotter_fetch_base_urlif the card must keep linking elsewhere.- Some links are skipped under load (logged as “at capacity”). Raise
max_concurrency, or lowermax_image_workersif memory is the limit.