I wanted an automatic, timestamped record of one Signal conversation without having to sit there taking screenshots myself. Signal Desktop needs a screen to run on, so I put it in a Docker container with a virtual display and a small script that takes a picture of that display every 20 minutes. It runs in the background on my Windows PC and quietly fills a folder with screenshots.
Here’s the whole setup, so you can build it yourself.
Why This Helps If You’re Being Harassed
This is especially useful if someone is harassing you on Signal with disappearing messages. Normally, to keep a record you’d have to open every message yourself and screenshot it before it vanishes. That means reading things you’d rather not see, and racing a timer. With this setup, the container keeps the conversation open and captures it for you, so you can gather a dated record without having to look at the messages as they come in.
A few things to know so you don’t miss anything:
- Set the screenshot interval shorter than their disappearing-message timer. If their messages disappear after 5 minutes and you screenshot every 20, most of them will be gone before they’re captured. When in doubt, go short, such as every 60 seconds.
- The container still counts as opening the messages. Signal treats it as one of your linked devices, so when it shows a message it’s marked as read and the disappearing timer starts, the same as if you’d opened it on your phone. The difference is that you don’t have to look at it.
- Each screenshot only shows what fits on the screen. If a lot of messages arrive at once, some can scroll out of view before the next capture. A taller screen (for example
SCREEN_HEIGHT=1600) and a shorter interval both help. - View-once photos and videos won’t be captured. Signal Desktop can’t open them.
- Keep the screenshots somewhere safe and back them up. The file names include the date and time, which helps if you need to show them to the police or a lawyer later.
How It Works
- The container runs Debian with Signal Desktop installed from Signal’s official repository.
- A virtual screen (Xvfb) gives Signal somewhere to draw, and noVNC lets you see and use that screen from your web browser.
- A screenshot script saves the whole screen as a PNG on a timer you choose.
- A health check and a watchdog restart Signal if it stops responding, and Docker restarts the container if things go really wrong.
What You Need
- A Windows PC with Docker Desktop installed and running.
- Your phone with Signal on it, to link the new desktop device.
Step 1: Create the Build Files
Make a new folder (for example C:\signal-docker) and save each of the five files below into it, using exactly these file names.
Dockerfile
FROM debian:12-slim
ENV DEBIAN_FRONTEND=noninteractive \
DISPLAY=:0 \
SCREEN_WIDTH=1280 \
SCREEN_HEIGHT=800 \
SCREENSHOT_INTERVAL_SECONDS=1200
# Base packages for X, VNC, noVNC, D-Bus, screenshots, fonts, and utils for healthchecks
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
ca-certificates curl wget gnupg \
x11vnc xvfb fluxbox novnc websockify python3-websockify \
x11-apps imagemagick dbus xdg-utils x11-utils \
fonts-noto-color-emoji fonts-noto-cjk; \
rm -rf /var/lib/apt/lists/*
# Install Signal Desktop from the official APT repository
RUN set -eux; \
mkdir -p /usr/share/keyrings /etc/apt/sources.list.d; \
wget -O- https://updates.signal.org/desktop/apt/keys.asc | gpg --dearmor > /usr/share/keyrings/signal-desktop-keyring.gpg; \
wget -O /etc/apt/sources.list.d/signal-desktop.sources https://updates.signal.org/static/desktop/apt/signal-desktop.sources; \
apt-get update; \
apt-get install -y --no-install-recommends signal-desktop; \
rm -rf /var/lib/apt/lists/*
# Copy scripts
COPY start.sh /usr/local/bin/start.sh
COPY screenshot-loop.sh /usr/local/bin/screenshot-loop.sh
COPY healthcheck.sh /usr/local/bin/healthcheck.sh
COPY watchdog.sh /usr/local/bin/watchdog.sh
RUN chmod +x /usr/local/bin/start.sh /usr/local/bin/screenshot-loop.sh /usr/local/bin/healthcheck.sh /usr/local/bin/watchdog.sh
EXPOSE 5800 5900
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD /usr/local/bin/healthcheck.sh
CMD ["/usr/local/bin/start.sh"]
start.sh
#!/usr/bin/env bash
set -euo pipefail
export DISPLAY=${DISPLAY:-:0}
export SCREEN_WIDTH=${SCREEN_WIDTH:-1280}
export SCREEN_HEIGHT=${SCREEN_HEIGHT:-800}
# ----- Wait for outbound network/DNS to be ready -----
retries=30
until curl -fsS --connect-timeout 3 --max-time 5 https://updates.signal.org >/dev/null 2>&1; do
retries=$((retries-1))
if [ $retries -le 0 ]; then
echo "[start] Network not ready after multiple attempts; proceeding anyway" >&2
break
fi
echo "[start] Waiting for network..." >&2
sleep 2
done
# ----- D-Bus: clear stale pid and start only if not running -----
mkdir -p /run/dbus || true
if pgrep -x dbus-daemon >/dev/null; then
echo "[start] D-Bus already running"
else
if [ -e /run/dbus/pid ] && ! kill -0 "$(cat /run/dbus/pid 2>/dev/null)" 2>/dev/null; then
echo "[start] Removing stale /run/dbus/pid"
rm -f /run/dbus/pid
fi
dbus-daemon --system --fork --print-address || true
fi
# ----- Xvfb: clear stale lock and start only if not running -----
if pgrep -x Xvfb >/dev/null; then
echo "[start] Xvfb already running on $DISPLAY"
else
if [ -e /tmp/.X0-lock ] && ! kill -0 "$(cat /tmp/.X0-lock 2>/dev/null)" 2>/dev/null; then
echo "[start] Removing stale /tmp/.X0-lock"
rm -f /tmp/.X0-lock
fi
Xvfb "$DISPLAY" -screen 0 ${SCREEN_WIDTH}x${SCREEN_HEIGHT}x24 &
fi
# Wait for X to be responsive before continuing (up to ~30s)
for i in $(seq 1 30); do
if xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then
echo "[start] X display is ready"
break
fi
echo "[start] Waiting for X display..."
sleep 1
done
if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then
echo "[start] X display not responding; exiting to let Docker restart" >&2
exit 1
fi
# Window manager
if pgrep -x fluxbox >/dev/null; then
echo "[start] fluxbox already running"
else
fluxbox &
fi
# VNC & noVNC
if pgrep -x x11vnc >/dev/null; then
echo "[start] x11vnc already running"
else
x11vnc -display "$DISPLAY" -forever -shared -rfbport 5900 -nopw &
fi
if pgrep -f "websockify .* 5800" >/dev/null; then
echo "[start] websockify already running"
else
websockify --web=/usr/share/novnc 5800 localhost:5900 &
fi
# Screenshot loop in background (one instance)
if pgrep -f "/usr/local/bin/screenshot-loop.sh" >/dev/null; then
echo "[start] screenshot loop already running"
else
/usr/local/bin/screenshot-loop.sh &
fi
# Launch Signal Desktop (ensure only one instance)
if pgrep -f "signal-desktop" >/dev/null; then
echo "[start] signal-desktop already running"
else
signal-desktop --no-sandbox --disable-gpu --disable-software-rasterizer --use-tray-icon=false &
fi
# Start watchdog (one instance)
if pgrep -f "/usr/local/bin/watchdog.sh" >/dev/null; then
echo "[start] watchdog already running"
else
/usr/local/bin/watchdog.sh &
fi
# Keep container alive
wait -n || true
sleep infinity
screenshot-loop.sh
#!/usr/bin/env bash
set -euo pipefail
export DISPLAY=${DISPLAY:-:0}
mkdir -p /config/screenshots
: "${SCREENSHOT_INTERVAL_SECONDS:=1200}"
while true; do
ts="$(date +'%Y-%m-%d_%H-%M-%S')"
xwd -root -silent | convert xwd:- "/config/screenshots/signal_${ts}.png" || true
sleep "${SCREENSHOT_INTERVAL_SECONDS}"
done
healthcheck.sh
#!/usr/bin/env bash
set -euo pipefail
# 1) Check noVNC is serving (200/301)
if ! curl -fsS --connect-timeout 3 --max-time 5 http://localhost:5800/ >/dev/null; then
echo "noVNC not responding" >&2
exit 1
fi
# 2) Check Signal process exists
if ! pgrep -f "signal-desktop" >/dev/null; then
echo "signal-desktop process not found" >&2
exit 1
fi
# 3) Check X display
if ! xdpyinfo -display "${DISPLAY:-:0}" >/dev/null 2>&1; then
echo "X display not responding" >&2
exit 1
fi
exit 0
watchdog.sh
#!/usr/bin/env bash
set -euo pipefail
export DISPLAY=${DISPLAY:-:0}
fail_count=0
max_fail_before_restart=3
max_fail_before_exit=6
while true; do
if /usr/local/bin/healthcheck.sh >/dev/null 2>&1; then
fail_count=0
else
fail_count=$((fail_count+1))
echo "[watchdog] healthcheck failed ($fail_count)" >&2
# Do NOT start X or dbus here; only manage the app and let restart policy handle the rest
if [ $fail_count -eq $max_fail_before_restart ]; then
echo "[watchdog] restarting signal-desktop" >&2
pkill -f signal-desktop || true
signal-desktop --no-sandbox --disable-gpu --disable-software-rasterizer --use-tray-icon=false &
fi
if [ $fail_count -ge $max_fail_before_exit ]; then
echo "[watchdog] too many failures; exiting to trigger Docker restart" >&2
exit 1
fi
fi
sleep 20
done
If you save the .sh files with a Windows editor, make sure they use Linux (LF) line endings. Otherwise the container won’t be able to run them.
Step 2: Create the Output Folders
Pick where the screenshots and settings should live. I use my D: drive. In PowerShell:
mkdir D:\Signal_Screenshots -ErrorAction SilentlyContinue
mkdir D:\Signal_Config -ErrorAction SilentlyContinue
Step 3: Build the Image
In the same PowerShell window, go to the folder with the build files and run:
cd C:\signal-docker
docker build -t signal-screenshots:latest .
Step 4: Start the Container
Change the folder paths, time zone and screenshot interval to suit you, then paste this into PowerShell. The interval is in seconds, so 1200 is one screenshot every 20 minutes. If you’re capturing disappearing messages, make it shorter than their timer (see above).
docker run -d `
--name signal `
--restart unless-stopped `
-p 127.0.0.1:5800:5800 `
-e TZ=America/Chicago `
-e SCREENSHOT_INTERVAL_SECONDS=1200 `
-e SCREEN_WIDTH=1280 -e SCREEN_HEIGHT=800 `
-v D:\Signal_Screenshots:/config/screenshots `
-v D:\Signal_Config:/config `
--log-opt max-size=10m `
--log-opt max-file=5 `
--shm-size=1g `
signal-screenshots:latest
A Quick Security Note
This build doesn’t put a password on the browser viewer. Anyone who can open it can read and type in your Signal account. That’s why the command above uses -p 127.0.0.1:5800:5800, which only lets this PC reach it. Don’t change that to -p 5800:5800 unless you want everyone on your network to have access. Also keep in mind the screenshots folder will hold your actual messages, so store it somewhere private.
Step 5: Link Signal
Open http://localhost:5800/vnc.html in your browser and click Connect. You’ll see Signal Desktop showing a QR code.
On your phone, open Signal and go to Settings → Linked devices → Link new device, then scan the code. If Signal offers to transfer your message history, say yes so older messages show up too.
Open the conversation you want to keep, scroll to the bottom, and then just close the browser tab. The container keeps running in the background.
Step 6: Check It’s Working
After the first interval has passed, look in your screenshots folder. You should see files named like signal_2026-10-22_14-20-00.png. If the folder stays empty, check the container’s logs with:
docker logs signal
Stopping, Restarting and Updating
docker stop signalanddocker start signalpause and resume it, and Signal stays linked.- To update Signal Desktop, rebuild the image with
docker build --no-cache -t signal-screenshots:latest ., remove the old container withdocker rm -f signal, and run the Step 4 command again. Signal’s own data lives inside the container, so a new container means linking Signal again from your phone.
That’s it. Once it’s linked, it just runs, and you’ve got a running visual history of the conversation without lifting a finger.
This post partially created with AI