# Transmute: a self-hosted converter whose quickstart pins nothing

> A Python and Qt style web app that converts and compresses files on your own server, shipped as a single Docker Compose download. The interesting parts are the unpinned image tag, the dependency list that disagrees with the image, and a maintainer who writes down the gaps.

**transmute-app/transmute** — Self hosted file converter and compression tool for images, video, audio, json, excel and more. Supports over 3,000 conversions!

- Repository: https://github.com/transmute-app/transmute
- Website: https://transmute.sh
- Stars: 1,422 · Forks: 99
- Language: Python
- License: MIT
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/transmute-app-transmute

## The quickstart pulls a compose file that pins no version

The entire quickstart is one line, and it writes to your working directory rather than to a project folder:

```bash
wget "https://raw.githubusercontent.com/transmute-app/transmute/refs/heads/main/docker-compose.yml" && docker compose up -d
```

That command fetches a compose file from the main branch and starts a container named transmute from ghcr.io/transmute-app/transmute:latest. Nothing in the command records which build you received. The tag is latest, so it follows every push to main, and the repository's most recent push was 2026-10-01, two days before the v2.1.0 release tag was cut. If you want a fixed build you have to edit the tag yourself; the three recent releases are v2.1.0 from 2026-09-29, v2.0.0 from 2026-06-30 and v1.3.1 from 2026-06-12, and nothing in the quickstart points at any of them.

The rest of the compose file is more careful. It maps 3313 to 3313 on the host, sets restart to unless-stopped, and keeps all state in a single named volume, transmute_data, mounted at /app/data. That path is the whole of your data: it is what you back up, and it is what you lose when someone runs docker volume rm. A commented line in the file shows the one extension point, mounting your own stylesheet read-only at /app/data/pdf/custom.css so that Markdown and document to PDF output uses your CSS instead of the built-in one. That tells you the PDF path reads a file out of the data volume rather than a setting from the environment.

The healthcheck carries the timing worth knowing. It runs wget against http://localhost:3313/api/health/ready every 30 seconds with a 10 second timeout, 3 retries and a start period of 40 seconds. Because the start period exceeds the interval, the container gets roughly 70 seconds of startup before a failed probe starts counting toward unhealthy. On a host that has to pull a multi-hundred-megabyte image and warm up an OCR and document toolchain, that budget is the difference between a slow start and a restart loop under the unless-stopped policy.

## requirements.txt pins PyOpenGL a version behind the image

The dependency file is flat and fully pinned at 51 entries, and reading it tells you more about the conversion surface than the format table does. Pillow 12.3.0 handles images, pandas 3.0.1 and pyarrow 23.0.1 handle structured data, weasyprint 70.0 and ocrmypdf 17.4.0 handle PDF output and OCR, PyMuPDF 1.27.2 and pdf2docx 0.5.8 handle PDF input, openpyxl 3.1.5 sits next to xlrd 2.0.2 for spreadsheets, yt-dlp 2026.7.4 pulls remote media, and pysubs2 1.8.0, vobject 0.9.9 and fonttools 4.61.1 cover subtitles, contacts and fonts. pytest 9.0.3 is in the same file, so a test dependency installs in production.

One line is annotated, and the comment says out loud that the file is not the whole truth. PyOpenGL is pinned at 3.1.0, which is what pyrender wants, but the comment above it says the Docker image force-reinstalls 3.1.7 so the OSMesa OSMesaCreateContextAttribs symbol resolves. So the file you install from locally is not the file the shipped image runs. The Makefile installs exactly that file:

```bash
$(PYTHON) -m pip install -r requirements.txt
cd frontend && npm install
```

Follow those two lines instead of Docker and you get the older OpenGL binding, on whatever machine you happen to be using. The backend is FastAPI 0.136.3 on uvicorn 0.49.0, so anything that renders a 3D scene or an animated icon goes down a different code path in your local checkout than in the container, and the difference is a comment in a dependency file rather than a documented requirement.

A second annotated line is shorter: toons is pinned at 0.5.3 with a trailing comment that links to an issue on the toons repository instead of explaining anything. A pin held in place by an upstream issue link is a pin somebody intends to revisit, which is fine for a single-maintainer project and worth knowing about if you plan to run it unattended.

## Seven themes in the feature list, eight in the theme table

The feature bullet promises seven built-in light and dark themes. The table directly beneath it names eight: Rubedo, marked as the default, plus Citrinitas, Viriditas and Nigredo on the dark side, and Albedo, Aurora, Caelum and Argentum on the light side. Four and four. Either the bullet was never updated after a theme was added, or one of the eight is not selectable, and nothing in the repository settles which. Screenshots for each entry are placeholders with no images behind them.

The names carry more than decoration. They are stages of the alchemical sequence, and the project is named after the operation itself. Rubedo is the final red stage, nigredo is the blackening that precedes it, citrinitas and viriditas are the yellow and green stages between them. A tool whose job is turning one file into another, with a compression mode that makes files smaller, offering a dark theme set that follows that sequence in order, is making a small joke and keeping it consistently.

The light side breaks the pattern. Albedo, the whitening, is a stage; Aurora, Caelum and Argentum are a dawn, a sky and a metal. So the dark themes read as a transformation in order while the light themes read as a palette, which is a reasonable design decision and also a hint that the alchemical naming arrived with the default theme rather than being planned as a set. Rubedo being marked as the default, in other words, is the load-bearing joke.

## Over 3,000 conversions on the repository, 100-plus formats in the features

The repository description advertises support for over 3,000 conversions. The feature bullet inside the project page says 100+ formats supported. Both numbers are plausible under different definitions and neither is defined: formats are not conversion pairs, and a pair count grows faster than a format count as the supported set grows in both directions. A reader trying to plan around the larger figure has to guess which definition produced it.

The list itself is not in the repository. The project page points to transmute.sh/conversions for the full list of supported formats and conversion pairs, which is a page on the hosted documentation site. No file in the tree enumerates coverage, so there is no offline answer to whether a particular pair works and no diff to read when a release changes it. For a tool whose central promise is that nothing leaves your server, the capability list is the one thing you have to fetch from the server operator's website to find out what the server can do.

Coverage also is not purely a property of the code, because several formats arrive as plugin packages. pillow-avif-plugin 1.5.5 and pillow-jxl-plugin 1.3.7 supply AVIF and JPEG XL encoding and decoding, and pillow_heif 1.3.0 supplies HEIF, all as compiled extensions layered on Pillow. An image format therefore counts only when that extension installs and imports on your architecture and your platform libraries, which is exactly the class of thing that differs between a local pip install and a prebuilt container image.

Archives follow the same pattern. cbz 4.0.0, rarfile 4.2.0 and py7zr 1.1.3 cover comic archives and other containers, brotli 1.2.0 and pyzstd 0.19.1 are the compression backends behind the compression mode, and extract-msg 0.55.0 reads Outlook messages while ezdxf 1.4.4 and trimesh 4.12.2 handle CAD and mesh files. The dependency list is the closest thing to a real format table in the repository.

## A Kubernetes manifest and an OIDC test compose ship undocumented

The top level of the repository holds four deployment files: docker-compose.yml, docker-compose-dev.yml, docker-compose-oidc-test.yml and k8s-deployment.yml, next to backend/, frontend/, docker/, docs/, scripts/ and assets/ directories and a .github/ workflow directory. Three of those four files appear nowhere in the project page. The quickstart uses one of them.

A Kubernetes manifest at the top level, rather than inside a deploy/ folder, is a deliberate placement for something intended to be found. It still needs to be found, and the only pointers the project page offers are a link to transmute.sh/docs/ and a hosted demo. A docs/ directory exists in the tree, but the documentation link goes to the hosted site instead of the directory, so the two can drift without anything in the repository noticing. Reading the local docs is a deliberate act, not the default path.

Single sign-on is the other half of this gap. The feature bullet spells it OiDC / SSO support, with the I transposed, and it is the only sentence in the file that tells a reader login through an external identity provider exists at all. What it does give you is the mechanism and one concrete provider, since it says account creation and login go through OIDC providers such as Authentik. A separate compose file named docker-compose-oidc-test.yml exists to exercise that path, so the feature is tested in the repository, but the shipped compose file contains no identity provider settings and pydantic-settings 2.14.2 is what would pick them up from the environment. Nothing in the quickstart tells you which variables to set.

## The public internet warning stops at telling you to add a proxy

A warning sits immediately above the quickstart. It says to think carefully before exposing the service to the public internet or a WAN, states that the app includes built-in authentication and per-user data isolation but is designed for trusted networks, and says that beyond a LAN you should place it behind a reverse proxy with TLS and rate limiting. It closes by saying the maintainers are not responsible for security issues arising from your deployment configuration.

That is a direct statement of where the trust boundary sits, and it is also the whole of the guidance. No example nginx or Caddy configuration appears in the repository, no header names are given, and the shipped compose file publishes port 3313 straight to the host with nothing in front of it. TLS is named but configured nowhere in the files you download, so the operator has to assemble the layer the warning assumes is handled. Rate limiting has the same shape: a request rate, a burst allowance or a single header name would have made the instruction actionable, and none of the three appears.

The API is a second surface on the same port, which makes the proxy question double. API key support and role-based access are both described as built in, and the OpenAPI documentation for the running app lives at http://TRANSMUTE_IP:3313/api/docs. That address is a placeholder hostname rather than something you can paste, and the schema page sits on the same port as the interface and behind the same authentication, so exposing it means deciding deliberately whether your API description is public. The video path has the same shape, since yt-dlp 2026.7.4 in the dependency list means the container can fetch remote media, which is an outbound network call in a service whose selling point is that nothing leaves the machine.

## The comparison table admits the gap to the services it replaces

A table titled What Does Transmute Replace? sets the project against CloudConvert.com, FreeConvert.com, Convertio.co, Vert.sh and ConvertX on three columns: no size limits, private, and free API. The first three services fail all three columns. Vert.sh and ConvertX pass size limits and privacy and fail the API column. Transmute is the only row with three check marks.

The italic line above the table is the most honest sentence in the file: for the record, I love all of these services and use them all frequently, Transmute is not up to par with any of them yet. The maintainer states the gap in the first person rather than hiding it, and the table's shape agrees with that admission. The differentiator is not conversion quality or format coverage, since nothing in the project claims either. It is the combination of self-hosting with an HTTP API, which is exactly the column the other two private alternatives fail.

Read as a buying guide, then, the table says one thing: pick this over a hosted converter when privacy and API access matter more than breadth, and pick a hosted service when you need a format it cannot yet produce. The catch from the format section applies directly. Whether the local build covers the pair you need is answered on a hosted web page rather than in the repository, so the comparison that justifies the choice is also the one you cannot verify offline.

## Machine-written contributions are declined on policy, not on review

A note near the top of the project page states that this is a human-led, maintainer-reviewed project. AI tools assist during development, and the note is specific about where: autocomplete, some boilerplate, and help with tests. Everything else is written, reviewed and validated by a human who understands the result and takes responsibility for it. The note adds that this is not an autonomously generated project and that fully AI-generated or agent-submitted contributions are not accepted, leaving the detail to a contributing guide.

Stating this in the project page rather than only in the contributing guide is unusual, and it has a mechanical consequence. Automated pull requests are declined by policy before anyone reads them, so the fastest route to a contribution is the slowest one. For a small project that is a defensible trade, and the scale supports it: 1,402 stars, 98 forks and 24 open issues, a last push on 2026-10-01, and three releases inside four months with v2.1.0 following v2.0.0 by three months.

The dependency list explains why the policy has teeth. Fifty-one pinned entries across imaging, video, documents, spreadsheets, archives and CAD have to keep receiving security and compatibility updates, and the maintainers are doing that from a project that has closed off automation as a way to widen the contributor pool. Whether that is sustainable is not something the repository answers, but it is the constraint that sits behind the human-led note, the pinned versions and the three-month gap between major releases.

## Conclusion

Transmute suits a homelab, an internal network or a privacy-sensitive shop that needs an HTTP API nobody else will meter. It is the wrong choice for a public upload service, because the deployment boundary is described as your problem, and for anyone who needs a definite answer about format coverage offline, because that list lives on a hosted site rather than in the repository. Before you rely on it, pin the image tag yourself, check that the format you actually need is in the container you pulled, and decide who owns the reverse proxy before the first request rather than after.

## FAQ

### How do I start Transmute for the first time?

Run the one-line quickstart, which downloads docker-compose.yml and brings the container up, then open localhost:3313. The container listens on port 3313 and keeps its state in a named volume called transmute_data mounted at /app/data.

### Does Transmute send my files to a third party?

No. Files are processed on your own server. One exception to plan for is the yt-dlp dependency, which lets the container fetch remote media, so converting a URL is an outbound request rather than a local read.

### Does Transmute really have a free API?

In its own comparison table, yes: Transmute is the only option marked for no size limits, privacy and a free API at once, while Vert.sh and ConvertX fail the API column. The OpenAPI docs for a running instance are served at /api/docs on port 3313.

### Can I expose Transmute to the public internet?

The project warns against it without giving a configuration. It says the app has authentication and per-user data isolation but is built for trusted networks, advises a reverse proxy with TLS and rate limiting beyond a LAN, and states the maintainers are not responsible for problems caused by your deployment configuration.

### How do I pin a specific Transmute version?

Edit the compose file yourself. It pulls ghcr.io/transmute-app/transmute:latest, and the quickstart downloads that file from the main branch, so nothing shipped records a build. Recent tags are v2.1.0 from 2026-09-29, v2.0.0 from 2026-06-30 and v1.3.1 from 2026-06-12.

### What themes does Transmute ship with?

Eight are named, even though the feature list says seven. The dark set is Rubedo, marked as the default, plus Citrinitas, Viriditas and Nigredo; the light set is Albedo, Aurora, Caelum and Argentum.

## Sources

- [License: MIT](https://github.com/transmute-app/transmute/blob/main/LICENSE)
- [Project website](https://transmute.sh)
- [README](https://github.com/transmute-app/transmute/blob/main/README.md)
- [Releases](https://github.com/transmute-app/transmute/releases)
- [transmute-app/transmute on GitHub](https://github.com/transmute-app/transmute)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/transmute-app-transmute
