Nginx Unit – Universal web app server
github.com
github.com
Also, Docker environments running PHP via Nginx Unit will no longer need separate containers for http + fpm, as it works similar to Apache's mod_php.
1. https://habr.com/en/articles/646397/
2. https://medium.com/@le_moment_it/nginx-unit-discover-and-ben...
3. https://github.com/nginx/unit/issues/6#issuecomment-38407362...
in my experience those are the ones that kick and scream when they don’t start as root.
https://github.com/sameersbn/docker-postgresql/issues/30#iss...
Curious about benchmarks and tests of TCP vs Unix domain sockets between docker containers.
Volume config: https://github.com/api-platform/api-platform/blob/main/docke...
Caddy config: https://github.com/api-platform/api-platform/blob/main/api/d...
php-fpm config: https://github.com/api-platform/api-platform/blob/main/api/d...
I'm also curious on the performance differences between containers.
"Unix Domain Sockets vs Loopback TCP Sockets"
https://nicisdigital.wordpress.com/2014/03/03/unix-domain-so...
Hn submission: https://news.ycombinator.com/item?id=37466475
Unfortunately said benchmarks are usually done by the software company making said webserver, so have to be taken with a pinch of salt.
1. https://www.litespeedtech.com/open-source/litespeed-sapi/php
1. Does Unit + PHP doing the typical “initialize & teardown” that PHP is know for?
Or is Unit persisting the initialization/setup, hence why it’s achieving the way faster results?
2. How is Unit/PHP architecturally different than NGX-PHP (an event loop)?
Here's a page with the features: https://unit.nginx.org/keyfeatures/
The languages supported, if anyone is curious:
Binary-compiled languages in general: using the embedded libunit library.
Go: by overriding the http module.
JavaScript (Node.js): by automatically overloading the http and websocket modules.
Java: by using the Servlet Specification 3.1 and WebSocket APIs.
Perl: by using PSGI.
PHP: by using a custom SAPI module.
Python: by using WSGI or ASGI with WebSocket support.
Ruby: by using the Rack API.
WebAssembly: by using Wasmtime.
I'm currently using Apache as a reverse proxy for most apps and sometimes PHP-FPM when I need to run software with PHP, which works well for my personal needs (mod_md and mod_auth_openidc are pretty cool), but it's cool to see something similarly interesting to OpenResty coming along as well, too!It's not so much "on the fly", as it is moving the long-term config storage to a different system.
An API and a web server with small segmented updates make this so much easier. Compare this to Apache, that has to wait to properly end existing connections before reloading, has a config file parsing overhead, and probably does not scale that well with several virtual hosts anyway. There are hardware/File System level limitations as well.
But for API gateway or any kind of sidecars? Very valuable.
Infrastructure As Code (in all its forms, chef/puppet/ansible/tfe etc.) is the standard for all enterprise cloud setups these days. It makes sense to support that as a first class feature.
This is infrastructure-as-state
You have to manage automatically loading configs and keeping in sync with what you have in code, instead of deploying with a static configuration file
In this case, I think Nginx is trying to avoid the in-fighting that happens between platform teams and the various teams that they serve. Most platforms with static configuration don't make it easy to ACL the platform so that one user can't mess with another user. E.g. it would be hard to automatically prevent one user from trying to take another's domain name.
This config API could be ACL'ed so that you can update your application code, but not change it's domain name or IP. Or whatever else the platform team wants to cut off. This hopefully comes with ACLs like that, but if it doesn't you could always add it in a standard way with a reverse proxy that has ACLs. That's a lot easier to do than trying to write an nginx config parser to make sure platform users aren't tinkering with a particular setting in their config.
Restarting the binary means you'll lose requests while it's restarting, so adding php (or whatever) support on the fly is what you need when running a system where losing those requests is material. Which it won't be for most people, but for, eg, Google (who don't use Nginx), losing those requests is a problem.
I suppose arguably that becomes a bit trickier for containers, so perhaps that's why you'd want to configure via an API?
I would also say it's easier to enforce good "IaC hygiene" when the configs are managed via configuration files. They can go through a code review process, deployed via existing config management systems etc.
For most workloads it should be entirely possible to deploy a new stateless config and not need to resort to using mutable state for critical infrastructure.
If you have long-lived, stateful connections (perhaps for live streams) then I can see why re-configuring in place would be desirable, but in my experience that's pretty rare.
Trying to find context on what it was, I saw it’s been on HN a few times (mostly to the sound of people asking the same question).
Since I mostly do php/laravel, I was pleasantly surprised that it let me remove php-fpm from my stack - extremely nice when putting PHP apps in a container.
I personally have come to dislike PHP-FPM’s max_children and similar settings, whose low defaults come as an annoying surprise when you suddenly get gateway errors before your server is even overloaded. (and having to add nginx AND php-fpm “layers” into a container to run PHP apps is all sorts of annoying).
Unit doesn’t have as many features as Nginx so I suspect it’s best used when there’s another fully-featured http layer in front of Unit (to more easily handle TLS, protecting dot files, gzip, cache headers, and the like).
Overall i’m excited to use it when making images for php apps.
I almost see myself jury-rigging some bespoke filesystem on-ramp to that REST configuration interface. Likely with at least half a dozen scary security compromises.
Then on the other hand I guess they've never been afraid of getting called opinionated and that's certainly much better than trying to be everything, for everyone, at the same time.
This may contribute to YAML's popularity in CI systems, because CI systems often need to be adjusted by someone who isn't super familiar with the syntax, and YAML is easier to intuit than TOML.
As an aside: it's always curious to see how the programming world has splintered into cliques that no longer hang out together.
NGINX Unit is a "Universal" web server without support for C++, Rust, or ASP.NET!
But PERL is supported, like the 1990s Linux cgi-bin world never went away.
> Binary-compiled languages in general: using the embedded libunit library.
From https://unit.nginx.org/keyfeatures/#supported-app-languages
Choice quote:
The benefits of using the assembly language for your web apps are immense:
You get all the bragging rights: coding in assembly while everyone is working safe and secure within the confines of their precious sandboxed languages is like doing a perfect triple somersault without safety mats.
Next: I recommend multi-threaded assembly code with self-modifying code and lock free data structures!So might as well ask why it doesn't support Delphi.
The Perl ecosystem has come a long way from CGI scripts just like everybody else has.
This isn’t the dark days of .NET Framework anymore.
It’s JSON controlled via Curl? And has a bunch of langue’s built in for some reason? The question becomes why?
Only the NGINX team would think to "simplify" loading a configuration file from the CLI by using curl to post it. smh...
For example, in this project we use nginx + php-fpm to serve some PHP app:
https://github.com/PrivateBin/docker-nginx-fpm-alpine
It uses s6 to handle nginx + php-fpm services, meaning we have to maintain configuration for s6 services (written in execline), nginx webserver configuration files (nginx proprietary format) and php-fpm (INI format).
I've created a fork that replaces that stack with nginx unit:
https://github.com/PrivateBin/docker-unit-alpine
No need for a service manager and both webserver and php configuration are in one file[1]. What is not well documented IMHO is that you don't have to load the config at runtime - if you place the JSON file in the (compile time changeable) /var/lib/unit directory it will be loaded on startup, similar to a traditional /etc config file. But it also will get updated at runtime, if configuration is uploaded on the config socket, making it persist service restarts, hence /var is more appropriate than /etc.
[1] https://github.com/PrivateBin/docker-unit-alpine/blob/master...
Thank you very much for this example, and calling out the config file option.
Do you find that you get any use from the config api in your setup (vs pushing a new container)?
I imagine it might be more useful if nginx unit acts like a k8s ingress like setup?
As for:
> both webserver and php configuration are in one file[1].
That's just the line in the Docker file:
COPY --chown=${UID}:${GID} conf.json /var/lib/unit/
I found the actual file more illuminating:https://github.com/PrivateBin/docker-unit-alpine/blob/master...
Even a small number of apps trying to get their own certificates at the same time can exhaust the Let's Encrypt quota for your domain, with serious consequences to your other online properties.
I got curious about this and decided to look it up, they actually have more restrictions than I expected[1]. Looks like to play it on the safe side you might be better off having a single server issuing certificates and distributing them where needed as well as using wildcards as much as possible. Interestingly, it looks like Google is doing just that since their certificate covers a very wide range of domains[2].
[1] https://letsencrypt.org/docs/rate-limits/
[2] https://www.sslchecker.com/sslchecker?su=b39e88e1c4d3efcdb79...
Edit: yes, it’s as straightforward as it sounds: https://unit.nginx.org/howto/integration/
It was first launched as closed source and the marketing gibberish f5 came up with made it sound like make believe.
This is done because web servers are optimised to handle (many) requests from clients (including things like TLS termination, optimised handling of static files, orchestrate to which backend service to send a request and much more), while application servers are optimised to run the application code.
In this model, the web and app servers run separately, for example in separate containers and they have to talk to each other so you have to somehow connect them (often this is done through a network socket).
So what Nginx Unit does is combining the two: it can do the standard web server stuff (not everything a normal nginx can do, but often that's not needed anyway), but also run your application for you, which could, depending on your setup, make your life a bit easier, as now you don't have to tie the two together.
What would be wrong with just using HTTP/2, maybe with a few custom headers sprinkled in for custom features?
In fact it's not one at all, nor does it claim to be. It's an app server.
Instead the docs have you do something manual with certbot (a complete nono if you believe in automatic SSL and are using docker images that don't persist data, as Docker is meant to be used).
The only reason I have SSL on my domain in the first place is that my host offers a simple setup that runs automatically.
docker volumes entered the chat
AppServe takes care of all that, and even if I did know the syntax by heart to do it in nginx, this is still faster.
Nginx Unit, with their python 2 module, on Debian 11 ended up working petty well.
Edit, yes: https://unit.nginx.org/howto/django/
Huge potential!
What I want to see for myself is how Unit compares to those for this reduced scope (api's/services) and how fast I can be productive with it. This could be a good starting point for a lot of new applications, kind of an "airflow (the data orchestration thing) on Webassembly".
So main points are performance (is it good enough) and developer experience (is it easy to maintain/change).
Nothing really ground breaking, just trying out stuff.
You don't need your language/runtime to support arm/other to run same compile artifact on arm32, arm64, power5 and amd64 - compile to wasm once, run anywhere (nginx unit would still need to support your target).
Your runtime theoretically don't need is support - compile to wasm, run on FreeBSD/arm.
You can compile your c microservice to wasm, and your rust microservice, and your c++ microservice to wasm - and deploy them sandboxed via the same stack - reducing complexity (note that another option is to deploy via nginx unit - different way to reduce complexity).
I check the comments next week and make sure to address the issues / ideas mentioned. If you don’t mind feel free to drop a comment here as well with your ideas / needs. https://github.com/nginx/unit/issues/945
I will work on a compression between Nginx and Unit to close this gap in our documentation.
Abstraction at it's finest.
You saw the lament in my comment though and that speaks a deeper truth.
If WSL took off quick enough we could have had lxc as the main player rather than the bastardisation of lxc that is docker.com
For an open source project they make it incredibly hard to access what are essentially text files for containerisation setups on windows or mac?
The days of cheap money are over, it's inevitable that certain SaaS companies will start tightening the screws on their users to match the returns they can get with cash in a bank. I just wish lxc (which docker was built off) got a chance to gain traction. It's miles ahead in DX and sure they serve different functionalities but can be used the same and the network effects can't be understated.
Can you point to some examples of this?
LXC has profiles which can be mixed and matched for containers, it's got far more extensibility, compose files are a one-shot creation that gets copy/pasted/modified all over the place (we're all guilty of this), you need to read every single compose file and reboot unlike additively popping another profile on a linux container while it's running.
I'd really disagree that compose files are somehow one-shot, or blindly modified. To the contrary, really, we have them checked in with the source code. Upon deployment to the cluster, the (running) services will be intelligently updated or replaced (in a rolling manner, causing zero downtime). LXC might be more elegant, but I have no idea what simple, file-based format I could use to let engineers describe the environment their app should run in without compose.
I need something that even junior devs can start up with a single command, that can be placed in the VCS along with the code, and that will not require deep Linux knowledge to get running. Open for suggestions here, really.
Not trying to be salty or anything – I really think Compose hits the sweet spot of abstraction which is less complex than both the monstrosities I listed and the ad hoc Bash scripts copying code over SSH and restarting services approach (so, the other extremity of declarative v. imperative).
Shameless plug: I also make a Docker Swarm dashboard, check it out: https://lunni.dev/
Lunni? cool, thank you for sharing it, I'll give it a try
How to navigate the docs? in https://lunni.dev I only reach https://lunni.dev/docs/install/ while I see a lot of good docs https://gitlab.com/lunni/lunni.dev/-/tree/master/docs ...maybe not ready for the website?
Edit: custom CSS for the homepage breaks navigation on inner pages on desktop. Should be fine now. Thanks for pointing this out!
Auto SSL wildcards was enough.
The difference is negligible to most people.
Isn't this what Nginx also does. How is Nginx Unit different beyond configuration via JSON REST APIs? The Github readme or their site is not very clear on this.
Slightly tangental, but it always irks me when I see these kinds of responses in JSON:
{
"success": "Reconfiguration done."
}
Really this should be something like "result": "success". Using "success" as a key name tells me nothing about the data it's representing.[1] eg one where the possible values are either Err(<some error>) or Ok(<some result>), and the data inside could be more complex types instead of just strings
His statement probably related to command line applications, but it makes sense for a lot of cases.
E.g. a command line app where you put a subtly wrong switch in that does exactly what it thought you wanted and prints nothing while outputting a 0 exit code is dangerous.
A tool should have sufficient interlocks to ensure safety when not engaged, but no more.
Which sounds good in theory, until you run into issues like this: https://www.youtube.com/watch?v=tLdRBsuvVKc
TLDW: Most of the Gitlabs outage where they deleted the primary prod and backup prod database could have been solved if they just waited for the command line op to complete - but because of a lack of feedback they thought it had frozen, leading to several other plans which made the situation worse.
I briefly evaluated it for bringing a PHP team into our Kubernetes cluster, but then ended up writing a bit of Go code to proxy into a real nginx+fcgi while adding a syslog sink (so PHP could log to stdout/stderr) and our standard prometheus metrics on the Go http server.
They ended up really getting into stripping out every piece of PHP they didn't use because the image built PHP from scratch, eventually took over maintenance of the go piece and ported a bunch of their apps to Symphony. I was in a platform engineering team and they were one of the few teams to really torture test every feature we ever shipped to the point they'd report edge cases or a bug to us every other week or so.
As for the logging question, we configured Symphony to log to syslog, which was provided by the Go daemon via unix socket.
jsonSuccessMessage:
type: object
description: "JSON message on success."
additionalProperties:
type: string
jsonErrorMessage:
type: object
description: "JSON message on error."
additionalProperties:
type: string
Yes, this is as bad as it looks: “success” isn’t even part of the schema. It’s in the examples, but not the actual schema definition. The way success or failure is actually indicated is by HTTP status codes. 200 is success, 400/404/500 is error. So, they seem to set very bad precedent in at least one of their response and their schema.But I was expecting it to be something like {"success": string} | {"error": string}, which is frankly a perfectly reasonable way of doing things: an untagged, but still unambiguous, union. It can make for quite pleasant code, too: `if response.success` and such.
You’re looking for a tagged union, something like {"result": "success" | "error", "message": string} (or alternatively like {"result": "success", "message": string} | {"result": "error", "code": string, …}), which is also a perfectly reasonable way of doing things. In some ways it can be viewed as more principled, as it more formally allows you to check the tag.
Really, the two approaches are much of a muchness. They each have their strengths and their weaknesses. But if general status is being done at the HTTP layer, I’d prefer {"message": "Reconfiguration done."}, or… well, actually, just a 204 No Content response, and no JSON or body at all.
(On the terms untagged and tagged unions as I’m using them: they’re necessarily a bit different from the concepts as exposed in languages like C, since you’re dealing with a model built on objects rather than bytes, but they’re still reasonable descriptions of them. In Serde’s classifications, they’d be the untagged and internally tagged enum representations <https://serde.rs/enum-representations.html>.)
Although I still think this is a bad example of an externally tagged representation. In the Serde example they have the key as "Request" then what follows is the request object. In this example, the "success" key is followed by an arbitrary string message, which isn't obvious at all.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Response {
Success(serde_json::Value),
Error(serde_json::Value),
}Having gone through a pretty heavy overhaul of an "OpenAPI" (full code in Elixir at https://github.com/etalab/transport-site/pull/3351), I stumbled on that exact type of problem!
At the scale of nginx, having automatic verification that the examples (and the output of the API in general) match the specification would be great.
At our scale, here is what really helped me go through the rework (and ensure we do not regress too easily):
- setting additionalProperties to "false" to detect key field "rot"
- using "required: [x,y,z]" on everything, and by default specify "all the property keys", with an opt-out (so that each time a developer adds a field later, it is considered mandatory, unless otherwise specified)
- use tooling during the tests: "assert_schema" (with OpenAPISpex) to ensure our API endpoints responses pass the spec (additionalProperties: false helps ensure we get an exception in case of key field rot, again!)
- even more useful: crawl our production most important endpoints and tweak the spec until everything is green (an example of useful use of Task.async_stream in Elixir, by the way) https://github.com/etalab/transport-site/pull/3351/files#dif...
It can be super frustrating for users to live with the uncertainty of the response of an API for sure, and I was happy to discover the Elixir tooling (OpenAPISpex in particular) worked so nicely once I understood what I had to do.
Thanks for the link though!
Putting the redundancy with 200 or not 200 aside, I sense a certain aesthetic quality in the {"success":string}|{"error":string} approach. Namely in how it adheres to keeping the schematic stuff on left side of the colon.
To simplify the builds we have a expressjs app that is a simple static server and proxy for API requests.
I can see something like this easily replace that - all we need is static files and a /API* endpoint to proxy - avoiding cors issues.
In such an approach, any http-level error would be a network, server maintenance or other unknown/fatal error type of things.
App errors are encoded in the response body in a app-defined way.
It's not a completely bogus way of handling things, as long as it is perfectly consistent throughout the project and properly documented, which is rarely the case.
{ status: 200, body: { "message": "Reconfiguration done." } }
?
{ "responseMessage": "Reconfiguration done.", "responseCode": 133, "isSuccessful": true }
This way you can map to a custom message on your front-end based on the responseCode whether its successful or not, you can fallback to the response message if no mapping is found, and you can easily check if the transaction was successful or not.
myapp.company.com {
reverse_proxy localhost:3000
}