nyaproxy ships a starter config with authentication disabled, and the image binds 0.0.0.0
NyaProxy acts like a smart, central manager for accessing various online services (APIs) – think AI tools (like OpenAI, Gemini, Anthropic), image generators, or almost any web service that uses access keys. It helps you use these services more reliably, efficiently, and securely.
At a glance
- What is it?
- A Python API gateway that injects upstream credentials, rotates across a key pool, enforces four rate limit scopes and persists cooldowns across restarts. The engineering is careful down to the EBUSY explanation for bind mounts; the default posture is not, since a fresh config has no api_key and the container command line ends with a public bind address.
- Who is it for?
- nyaProxy is a good fit for a team that already holds several keys for one upstream and wants to stop scattering them through application code, and a poor fit if you want a gateway you can expose without thinking about credentials. Three things decide it.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 83 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 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The starter config has no api_key, and the image defaults to a public bind address
On first run the gateway writes a starter `config.yaml` into the current directory and listens on `127.0.0.1:8080`. That config contains no `server.api_key`, which means authentication is disabled. The documentation is explicit that this is safe only while bound to loopback, and that you must set `server.api_key` before using `--host 0.0.0.0` or exposing the service any other way.
The container side is where the two defaults meet. The image's command line ends with `--config /app/config.yaml --host 0.0.0.0`, so an unmodified container listens on every interface. The compose file in the repository narrows that by mapping `127.0.0.1:8080:8080` on the host. The quick start, on the other hand, publishes the port the wide way:
mkdir -p data
cp configs/openai.yaml data/config.yaml # then replace every placeholder key
docker run -d \
-p 8080:8080 \
-v ${PWD}/data:/app \
--user "$(id -u):$(id -g)" \
k3scat/nya-proxy:latest --config /app/config.yaml --host 0.0.0.0That block has no port restriction and a config whose keys are placeholders, so the instruction in the file not to publish the port until `server.api_key` is set is the step that keeps this example from being an open relay to a paid upstream. Exposure is decided by two settings in two different places, which is one more than most deployments remember to check.
The first key in server.api_key is the master key
`server.api_key` is a list, and position inside it carries privilege. The first entry is the master key, used for the dashboard and the configuration interface; every further entry is an ordinary key for proxy traffic. There is no separate admin credential and no roles object, so the way to restrict who can change the gateway is to decide who holds the first entry.
That has a consequence worth stating before you rotate anything: reordering the list changes who can reach the configuration interface, and nothing in the file warns you that a reorder is a privilege change.
Four endpoints are exposed for humans and for monitoring. `/config` is the configuration interface with validation, `/dashboard` shows metrics, request history and queue status, `/info` lists the configured APIs and service status, and the container health check polls `/health` every 30 seconds with a 3 second timeout, a 10 second start period and 3 retries.
The interface will not tell you the request format. The last heading printed in the file is `### Request Forma`, and both that heading and its section end there, so how a client is expected to address an upstream lives outside this document. Work it out from the API list at `/info` or from the example configurations, and do not assume the request shape is documented.
The rest of the repository backs the operational story rather than the tutorial one: a `docs/` directory, a `scripts/` directory, an `assets/` directory, CHANGELOG.md, CONTRIBUTING.md, a pre-commit configuration and pytest.ini at the root. The readme ships in three languages, with README_ja.md and README_zh.md beside the English file. The badge row points at the PyPI project as the homepage, a Docker Hub repository under the maintainer's personal namespace rather than an organisation one, a coverage service, a wiki and a download counter. The distribution name on PyPI is nya-proxy, while the command you run is nyaproxy.
Mounting config.yaml instead of its directory returns 500 on every save
This is the sharpest failure mode in the project, and the documentation explains the mechanism rather than just the symptom. Saving from the configuration interface writes a temporary file and renames it over the target. A rename onto a bind mounted single file fails with `EBUSY`, so every save returns a 500. The same applies to a read only mount, since the rename cannot happen there either.
The fix is to mount the directory. The compose file does exactly that, with `- ./data:/app`, and the inline comment repeats the same two reasons: the rename, and the need for write access. So the mount has to be correct in shape and in permissions, and a config that works on your laptop can fail on the first save in a container.
The directory is not only for the configuration file. NyaProxy writes `.nya_state.json` into the same place, and that file is what carries rate limit windows and key cool downs across a restart, so a configuration change does not reset your quotas. A volume that is mounted but not writable blocks the state write as well as the save.
On ownership, the image runs as uid 100, which cannot write a directory you own, so the compose file passes `user: "${UID:-1000}:${GID:-1000}"` and the quick start passes `--user "$(id -u):$(id -g)"`. The stated alternative is `sudo chown -R 100:101 ./data`. Docker Desktop on macOS and Windows maps ownership for you, so the flag can be dropped there.
A file edit does nothing until you re-save through the interface
Configuration lives in a single YAML file validated against a bundled JSON schema, with ready made examples in the `configs/` directory. Applying it is where the surprise sits. Changes made through the `/config` interface trigger an automatic restart, and so do changes to the file, but only once you re-save through the interface. A hand edit on disk sits there doing nothing until someone opens the interface and saves.
That restart is the mechanism, not a side effect. The container file says it plainly: hot reload stays on because restarting is how a configuration change is applied, and passing `--no-reload` silently strands every edit made through the configuration interface.
Which puts the CLI reference and the container file in mild tension. The reference recommends `--no-reload` for production setups where restarts should be explicit. The container comment says that flag makes interface edits unappliable. Both are true, and the combination is a configuration with no working edit path: no automatic restart, and no manual restart mentioned for the interface. If you disable the supervisor, treat the interface as read only and plan your changes as image or command line changes.
The alternative to local files exists too. `--remote-url` with `--remote-api-key` and `--remote-app-name` pulls configuration from a remote config server, and that mode disables the local `/config` interface entirely. `--check-config` validates the schema and cross field configuration and exits, which is the safe way to test a file before a supervisor restarts on it.
Four rate limit scopes, and in the shipped example the endpoint cap bites first
Limits exist at four granularities: per endpoint, per upstream key, per client IP and per proxy user. The printed minimal configuration sets all four at once, along with a queue and a retry block:
server:
api_key:
- your_admin_proxy_key # first key = master key (dashboard + config UI)
- your_application_proxy_key # additional keys for regular proxy traffic
logging:
enabled: true
level: info
log_file: app.log
dashboard:
enabled: true
default_settings:
key_variable: keys
load_balancing_strategy: round_robin
queue:
max_size: 200
max_workers: 10
expiry_seconds: 300
rate_limit:
enabled: true
endpoint_rate_limit: 1000/h
key_rate_limit: 60/m
ip_rate_limit: 5000/d
user_rate_limit: 5000/d
retry:
enabled:Two observations. The last line is a key with no value, so the minimal configuration as printed is unfinished. And the numbers do not line up the way the names suggest: an endpoint cap of 1000 per hour works out to roughly 17 requests a minute, well under the 60 per minute allowed per key, so with these values the endpoint ceiling is what clients hit first and the per key ceiling is nearly four times looser. The daily caps of 5000 for IP and user sit above both.
The queue is what happens instead of a refusal: requests are held until configured quota becomes available, with a maximum of 200 waiting, 10 workers, and a 300 second expiry. So a burst queues for five minutes and then expires, which is a different failure from a fast rejection. Settings under `default_settings` apply to every API and any individual API block can override them, so these four numbers are defaults, not a single global policy.
Five routing strategies, and a quarantined key stays quarantined
Traffic is spread across a pool of keys by one of five strategies: `round_robin`, `random`, `least_requests`, `fastest_response` and `weighted`, with weights supplied through `key_weights`. The retry block covers the failure path: retry the status codes you select, cool down the key that failed, and rotate to the next one. Separately, key blocking removes a credential temporarily when it returns the upstream error statuses you configured.
The persistence detail is the one that changes behaviour across a deploy. `.nya_state.json` carries rate limit windows and key cool downs across restarts specifically so that applying a configuration change does not reset your quotas. The upside is that a hot reload cannot be used to clear a cooldown or to hand a caller a fresh quota by nudging the config. The cost is that a key poisoned by upstream errors stays out of rotation after every restart until that file is removed by hand, and nothing in the interface surfaces that as an action.
Request policy is the other half of the guard surface. `allowed_paths` and `allowed_methods` decide what may be forwarded before anything leaves the machine, and `request_body_substitution` can set or remove JSON fields under conditional JMESPath rules. That combination is what turns a key injector into something you can point at a browser-facing service, and it is also the setting to review before you let a client choose its own path.
Outbound traffic can be pushed through an HTTP or SOCKS proxy configured under `server.proxy`, which is the hook for egress control. Observability is a dashboard plus a Prometheus `/metrics` endpoint.
The overview sets out the trust model in one paragraph: applications call the gateway with an internal proxy key, and the gateway forwards each request to the configured upstream with the correct upstream credentials, rotating across a pool of keys while enforcing rate limits, retries and path policies. The client therefore never holds an upstream key, which is the argument for running this at all, and `key_variable: keys` in the default settings is what names the pool on the wire.
The same overview closes with a usage condition worth keeping in front of you during evaluation: use the gateway only with credentials and traffic patterns that the upstream service terms allow. Pooling several keys for one provider and rotating across them changes how quickly a quota the upstream allocated to you gets spent, and whether that is permitted is a question for those terms.
mypy only checks the modules that are already clean
The type checking setup carries its own admission. The configuration sets `follow_imports = "silent"`, and the comment above it says legacy modules are still type dirty, so they are analysed when imported but their errors are not reported. The next comment calls the module list a ratchet, stating that mypy currently checks only the modules known to be type clean, with a note to widen it as modules are cleaned up.
That is an honest arrangement, and it has a specific effect for anyone reading CI results: a clean type check means the clean set is still clean, not that the package type checks. Linting is separate and unqualified, with Ruff at a py311 target, line length 88, import sorting enabled, double quotes and space indentation.
The Makefile is where the intended workflow is written down. `make check` runs lint, typecheck and test and is described as the same quality checks as CI. Tests are split into unit and end to end, with the end to end run marked by the `e2e` marker, and coverage writes an HTML report to `reports/htmlcov`. Dev extras are pytest, pytest asyncio and pytest coverage; lint extras are mypy and Ruff. `pytest.ini` sits at the root next to a `uv.lock`, so development uses uv while the package itself builds with setuptools.
One loose end: the `clean` target runs `python .clean.py`, and no such file appears in the top level listing of the repository, so that target appears to have nothing to run.
One dependency arrives with three extras nobody documents
The runtime list is short and mostly self explaining: FastAPI, uvicorn, httpx with SOCKS support, JMESPath, orjson pinned below version 4, and prometheus-client. Two entries are worth a second look.
`watchfiles` is declared with a comment saying it is not imported directly, and that it is there for uvicorn's file watch backend to do config hot reload. It is a dependency you inherit rather than call.
`nacho-python` is required with the `remote`, `schema` and `server` extras. Three extras on a package the documentation never mentions is the largest undocumented surface in this project, and the extras line up with capabilities a reader can infer but cannot confirm: remote configuration, schema handling, and a server mode. Since the project also ships a bundled JSON schema and a remote config server mode, nacho-python is a plausible home for all three, which makes it infrastructure you are trusting with the request path rather than a small utility.
The rest of the metadata is straightforward. Python 3.11 or newer, classifiers through 3.14, development status Beta, version 0.8.2, MIT with the licence file declared. Release history is one busy day: v0.8.0, v0.8.1 and v0.8.2 all published on 20 July 2026, and nothing since, so the current build is a few months old and the version number has not moved through them.
The Dockerfile is two stage on a python:3.13-alpine base, installing into a virtual environment under /opt/venv and then running as a dedicated non login user whose home directory is /app, with port 8080 exposed and an MIT label. Root files also include CHANGELOG.md, CONTRIBUTING.md, a pre-commit configuration and a docs directory, so the project's own history and guidance live in the repository rather than only on a wiki.
Editorial conclusion
nyaProxy is a good fit for a team that already holds several keys for one upstream and wants to stop scattering them through application code, and a poor fit if you want a gateway you can expose without thinking about credentials. Three things decide it. Set server.api_key before anything binds a public interface, because the generated config leaves authentication off and the container command line ends with 0.0.0.0. Mount the configuration directory rather than the file, or every save through the config interface returns 500. And decide early whether you want the file watch supervisor on, because turning it off means an edit made through the interface never applies. Read the dependency list before installing: one package arrives with three extras the documentation never explains.
Frequently asked questions
Is nyaProxy authenticated by default?
No. On first run it writes a starter config.yaml with no server.api_key, which means authentication is disabled, and it listens on 127.0.0.1:8080. The documentation states this is safe only while bound to loopback, and that server.api_key must be set before using --host 0.0.0.0 or exposing the service.
Why does saving through the nyaProxy config interface return 500 in Docker?
Because the save writes a temporary file and renames it over the target, and that rename fails with EBUSY against a bind mounted config.yaml file. Mount the directory instead, and make it writable, or a read only mount blocks saves for the same reason. The repository's compose file mounts ./data:/app.
Does nyaProxy have a separate admin credential for the dashboard?
No. server.api_key is a list where the first entry is the master key used for the dashboard and the config interface, and any additional entries are ordinary keys for proxy traffic. Reordering the list therefore changes who can reach the configuration interface.
What does --no-reload do in nyaProxy?
It disables the config file watch supervisor, so configuration changes require a manual restart. The container file notes that this silently strands every edit made through the config interface, since restarting is how a configuration change gets applied.
What are the default rate limits in nyaProxy?
Four scopes are configured: endpoint_rate_limit 1000/h, key_rate_limit 60/m, ip_rate_limit 5000/d and user_rate_limit 5000/d, alongside a queue of 200 with 10 workers and a 300 second expiry. These sit under default_settings, which apply to every API unless an individual API block overrides them.
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/nya-foundation-nyaproxy)