Ownfoil runs a Switch shop from a container and warns you off its own website
Nintendo Switch library manager, with automated management tasks, serving your library to multiple supported clients on your Switch, with shop customization and multi user authentication.
At a glance
- What is it?
- Ownfoil turns a folder of console content into a browsable shop on port 8465 and serves it to clients on the Switch. The Python packaging, the compose file and the caution callout disagree with each other in ways worth reading before you deploy.
- Who is it for?
- Ownfoil suits you if you already have a content folder, a machine that can run a container, and a reason to want the shop under your own control.
- Can I use it commercially?
- Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 2, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Nine links with empty alt text, then a warning not to follow them
The top of README.md is a row of nine Markdown links with empty link text, so a reader sees a wall of badges instead of labels. The destinations name the repository itself, the latest release page, the Docker Hub repository, its tags page, an Unraid Community Apps listing, a Reddit thread, the Tinfoil download page, and two alternative Switch clients. Two paragraphs later comes a CAUTION callout saying there is no website associated with this project, only this GitHub repo, that Ownfoil is not released as an application or an executable file, and that nothing related to it should be downloaded or executed from outside the repository and its instructions.
Both halves are true at once, and which one you read first changes your next move. The badge row points at image tags and an app listing, which is exactly the shape of a product download. The callout forbids treating those artifacts as one. The practical reading is that the repository is the distribution channel and the registry is a convenience mirror, so before you pull an image tag you have no way to check what went into it.
Five install paths, and the tree holds something for each
Installation is delegated to a second file. Five anchors are offered: Using Docker, Using uv with a parenthetical note that Windows users do this, Using Unraid, Using Proxmox LXC, and Using the Helm chart. Every one of them has a counterpart at the top level of the repository: a Dockerfile, a docker-compose.yml, a docker/ directory that supplies the entry script, a chart/ directory for the Helm route, and a .devcontainer/ for a development checkout. Configuration is delegated as well, to Usage.md, which gets linked twice, once pointing at a first steps section and once at a settings reference.
The result is that the README is a table of contents with a feature list attached, and the operational detail lives one file over. Read the five paths as five different prerequisites rather than five flavours of the same thing: four need a container runtime of some kind, and the uv path needs a Python interpreter at 3.11 or newer, which is the floor declared in pyproject.toml.
The compose file ships sample passwords in its comments
docker-compose.yml is short enough to read end to end. It declares one service, pulls a1ex4/ownfoil:latest, sets PUID=1000 and PGID=1000 under the comment "For write permission in config directory", mounts three volumes from /your/game/directory to /games and from ./config and ./data into /app, and publishes exactly one port, 8465 to 8465. Underneath the active lines sit four commented variables, and they are the reason to read the file closely:
environment:
# For write permission in config directory
- PUID=1000
- PGID=1000
# to create/update an admin user at startup
# - USER_ADMIN_NAME=admin
# - USER_ADMIN_PASSWORD=asdvnf!546
# to create/update a regular user at startup
# - USER_GUEST_NAME=guest
# - USER_GUEST_PASSWORD=oerze!@8981Those comments describe creating or updating a user at startup, and the sample values are still literal passwords sitting in a public repository. Uncommenting the block without editing them leaves a service published on 8465 with a known credential pair. There is no certificate, no secret and no reverse proxy anywhere in this file, and the port mapping as written is not limited to a loopback address. Multi user authentication and console keys management are both feature lines in this project, and neither changes what the compose file does.
gunicorn is skipped on Windows in one dependency list and not the other
Two dependency lists ship side by side and they disagree exactly once. pyproject.toml pins fifteen packages under [project].dependencies, and one of them carries an environment marker:
dependencies = [
"flask==3.1.3",
"flask-login==0.6.3",
"gunicorn==26.2.0 ; sys_platform != 'win32'",
"nstools==2.0.0b7",
"nsz==5.0.0",
"zstandard==0.25.0",
]requirements.txt carries the same fifteen packages in the same order with no marker on gunicorn. The image build installs from requirements.txt, so a container always has a WSGI server whatever the platform, while a local install resolved from pyproject.toml leaves Windows without one. Nothing in the Docker path exercises that branch, which tells you the marker exists for the uv route named in the install list. The two files also differ in letter case on Flask-Login, which pip treats as the same name. Everything from flask==3.1.3 through zstandard==0.25.0 is pinned with an equality sign in both files, so neither path picks up a newer dependency on its own.
Content identification leans on a pre-release pin
The feature list promises content identification using content decryption or filename, and the dependency list is where the two paths become concrete. Decryption runs through nstools, pinned at 2.0.0b7 in both dependency files. A build numbered b7 is a pre-release rather than a stable one, and an equality pin means a deployed install stays on that exact build until someone edits the file. Compression is handled by nsz==5.0.0 with zstandard==0.25.0 underneath, and the credits credit the nsz format and tool to @nicoboss while @seiya-dev is credited for NSTools. Filesystem watching is watchdog==6.0.0, which is what would let the automatic organization and verification tasks notice a new file.
Filename matching is the other identification route, and it is the one where a wrong answer leaves no trace in the package metadata. Nothing visible says what happens when a filename disagrees with the content, whether a mismatch is flagged or accepted, or how the console keys management feature interacts with either path. The same applies to multiple clients support: the page links Tinfoil, CyberFoil and Sphaira as neighbouring projects, but no file in the tree states which clients the build speaks to.
The wheel relocates the app directory that the image copies in place
The repository keeps the application in two arrangements. The Dockerfile starts FROM python:3.14-alpine, copies the whole app directory to /app, copies docker/run.sh to /app/run.sh, creates /app/data, sets WORKDIR /app, and launches that script through an exec form ENTRYPOINT:
FROM python:3.14-alpine
RUN mkdir /app
COPY ./app /app
COPY ./docker/run.sh /app/run.sh
RUN mkdir -p /app/data
WORKDIR /app
ENTRYPOINT [ "/app/run.sh" ]pyproject.toml ships a different layout for anyone installing the package. It builds with hatchling, sets packages = ["ownfoil"], and adds a force-include line that maps the app directory to ownfoil/_app, so the same files the container expects at /app arrive inside the wheel as a private subpackage instead. That wheel also declares a console script, ownfoil bound to ownfoil.cli:main. It reads oddly next to a caution that says the project is not released as an executable file, though in practice a console entry point inside a Python package is not the downloadable binary that callout is guarding against.
An Alpine image that installs a C toolchain on arm and deletes it again
The image branches on a build argument immediately after its base line. For linux/arm/v6 and linux/arm/v7 it installs build-base gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev, which is a graphics and image stack, and it skips them on every other target:
ARG TARGETPLATFORM
RUN apk update && apk add --no-cache bash sudo \
&& if [ "$TARGETPLATFORM" = "linux/arm/v6" ] || [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
apk add --no-cache build-base gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev; \
fiAfter the pip install it runs apk del with the same package list on those arm targets, so the compiler and headers leave the image by the same path they arrived. That pattern points at where a dependency has no usable wheel and gets built from source, and it means the arm32 builds reach a different set of installed artifacts than the amd64 and arm64 builds from the same requirements.txt. The base image is also further ahead than the packaging metadata: python:3.14-alpine against a requires-python of >=3.11, so the container exercises one interpreter while the metadata claims a range starting three minors earlier.
Three tagged releases in six weeks, and the tree version matches
The version recorded in pyproject.toml is 2.4.2, and the newest release tag is also 2.4.2, dated 2026-09-29. Behind it sit 2.4.1 from 2026-09-08 and 2.4.0 from 2026-08-18, so three tagged releases landed inside roughly six weeks. The repository was last pushed on 2026-10-01 and is not archived. That is the whole maintenance picture available from the outside.
No changelog appears in the visible files, so there is no way to tell from the tree whether a given bump was a fix, a dependency refresh or a behavior change, and the classification is not published either. The repository does carry a tests/ directory and a .github/ directory, which tells you the project has somewhere to put tests and where its automation would live, but not what either contains. The one stability signal on the page is the Development Status :: 5 - Production/Stable classifier in pyproject.toml, and a classifier is written by a project's author rather than measured.
Editorial conclusion
Ownfoil suits you if you already have a content folder, a machine that can run a container, and a reason to want the shop under your own control. Settle three things first: which clients you actually run, since the client list is not written down anywhere in the tree; whether you will use the startup user variables, and if so whether you replaced the sample passwords left in the compose comments; and what the two identification modes do when a filename disagrees with the bytes. Take the caution callout at face value. The repository is the only source for this project, so a tag on an image registry or a listing in an app store is a rebuild of somebody else's copy, not something the project publishes. Nothing here changes what you are entitled to put in that folder.
Frequently asked questions
What is Ownfoil?
A self-hosted Nintendo Switch library manager written in Python. It points at a content folder, organizes, verifies and compresses it, identifies each item by content decryption or by filename, and serves the result as a customizable shop to several clients on the Switch. It also manages console keys and offers multi user authentication. The project publishes no website of its own, only the GitHub repository.
What runs on the Switch when you use Ownfoil?
The client, not the server. Ownfoil runs on a host machine and serves the library to supported clients running on the console, on port 8465 in the sample compose file. The host side needs Python 3.11 or newer when installed as a package, and the container image is built FROM python:3.14-alpine.
Can I install Ownfoil on Windows?
The five documented routes are Docker, uv, Unraid, Proxmox LXC and the Helm chart, and the uv route is the one marked for Windows users. That route installs the Python package, which declares requires-python of >=3.11, and its dependency set marks gunicorn as excluded when sys_platform is win32, so the Windows install has no WSGI server in it.
How does Ownfoil decide what a file is?
Two strategies are documented: content decryption and filename matching. Decryption runs through nstools, pinned to 2.0.0b7 in both requirements.txt and pyproject.toml, which is a pre-release build. Compression uses nsz==5.0.0 with zstandard==0.25.0 underneath, and watchdog==6.0.0 is what would notice new files for the automatic organization and verification tasks.
Is Ownfoil available as an app I can download?
No. A CAUTION callout states there is no website associated with the project, only the GitHub repo, and that Ownfoil is not released as an application or an executable file, with a warning against downloading or executing anything related to it from outside the repository. The packaging metadata does declare a console script named ownfoil bound to ownfoil.cli:main, and the Dockerfile builds the app from source.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/a1ex4-ownfoil)