Checks and rewrites uploads for the Gryt voice & video platform.
Pictures, video and emoji are decoded in a jail, written out again, and only then shown to anyone. Images go through Sharp, video through ffmpeg.
docker pull ghcr.io/gryt-chat/image-worker:latest
docker run -v gryt-data:/data --env-file .env ghcr.io/gryt-chat/image-worker:latestBrowse tags at ghcr.io/gryt-chat/image-worker.
It started out handling only pictures and was called the image worker. The docs and the app call it the media worker now, but the repository, the container image and IMAGE_WORKER_URL kept the old name. Renaming those would break every compose file and server that's already deployed, for nothing anyone would notice.
Compressing an avatar sounds like a utility job. This is the process that hands attacker-controlled bytes to an image decoder, so it's a review-required path in the AI policy and changes here get read line by line.
A video's poster is one frame that ffmpeg grabs and sharp turns into a JPEG.
The worker opens the file and hands ffmpeg the open descriptor, so ffmpeg never
opens a path itself. It may only use the mov/mp4 and matroska/webm demuxers and
the h264, hevc, vp8, vp9 and AV1 (libdav1d) decoders. It runs on one thread
with a 1 GiB address-space cap, and it's killed after 15 seconds. The flags are
in src/videoPoster.ts.
The Docker image builds its own static ffmpeg with nothing else in it: those
two demuxers, those decoders, the PNG encoder, and the fd and pipe
protocols. The versions and hashes are at the top of the Dockerfile, and a
new ffmpeg release means bumping them by hand.
In the image, ffmpeg doesn't run as the worker. The container starts as root
just long enough for jail/entrypoint.sh to start ffjail, and then the
worker runs as gryt. ffjail runs each decode as a second user, gryt-ff,
inside a chroot that holds the ffmpeg binary and nothing else. There's no /proc, /data or /app in there to read.
ffmpeg gets an empty environment, no capabilities, the 1 GiB cap, and a
seccomp filter that refuses sockets, ptrace and new processes. All it has is
the input as fd 3 and a pipe for the PNG. test/image checks that on every
pull request, with a probe that tries to read the worker's environment and
storage from inside the jail.
If you start the container as a non-root user, the jail can't start, and videos don't get a poster. The image never runs ffmpeg outside the jail.
Without ffmpeg, on a dev machine or your own build, videos just don't get a
poster. A system ffmpeg has to be 6.0 or newer, for the fd protocol. On Linux
the worker also needs prlimit from util-linux, and it skips posters rather
than run ffmpeg without the cap.
Running your own build in production (NODE_ENV=production) without the jail,
the worker doesn't touch the system ffmpeg at all, so videos get no poster. A
decoder bug in a stranger's file would run as you, with your files in reach.
If you accept that, set GRYT_ALLOW_HOST_FFMPEG=1.
| Variable | Default | Description |
|---|---|---|
DATA_DIR |
./data |
Path to the shared data directory (contains gryt.db) |
S3_BUCKET |
— | S3 bucket name (or subdirectory name for filesystem storage) |
STORAGE_BACKEND |
s3 |
Storage backend: s3 or filesystem |
S3_ENDPOINT |
— | S3-compatible endpoint (e.g. MinIO) |
S3_REGION |
auto |
S3 region |
S3_ACCESS_KEY_ID |
— | S3 access key |
S3_SECRET_ACCESS_KEY |
— | S3 secret key |
S3_FORCE_PATH_STYLE |
false |
Use path-style S3 URLs (required for MinIO) |
IMAGE_WORKER_CONCURRENCY |
2 |
Max concurrent image processing jobs (1–8) |
IMAGE_WORKER_POLL_MS |
1000 |
Database polling interval in milliseconds (250–10000) |
HEALTH_PORT |
8080 |
HTTP health check port |
HEALTH_HOST |
127.0.0.1 |
Address the health server binds to. Empty listens on every interface; the Docker image sets 0.0.0.0 |
yarn install
yarn devyarn build
yarn startFull docs at docs.gryt.chat/docs/deployment.
Please report bugs and request features in the main Gryt repository.
What sponsoring pays for, the tiers, and everyone who has sponsored: gryt.chat/sponsors. To sponsor: GitHub Sponsors.
The list itself lives in the Gryt README, in one place rather than ten, so it cannot fall out of step across repositories.