{"id":21543139,"url":"https://github.com/codeware-sthlm/enjinex","last_synced_at":"2026-04-05T22:02:52.644Z","repository":{"id":39629810,"uuid":"315235448","full_name":"codeware-sthlm/enjinex","owner":"codeware-sthlm","description":"Nginx reverse proxy with automatic Let's Encrypt renewals","archived":false,"fork":false,"pushed_at":"2023-04-25T21:07:51.000Z","size":1309,"stargazers_count":1,"open_issues_count":24,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-24T08:43:49.739Z","etag":null,"topics":["certbot","certificate","docker","jest","letsencrypt","managing-certificates","nginx","nodejs","nx-workspace","ssl","typescript","winston-logger"],"latest_commit_sha":null,"homepage":"https://github.com/codeware-sthlm/enjinex","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/codeware-sthlm.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-11-23T07:36:54.000Z","updated_at":"2023-12-20T19:02:37.000Z","dependencies_parsed_at":"2025-01-24T08:42:45.182Z","dependency_job_id":"62a719c0-235b-463d-bc5c-eb83b3c62056","html_url":"https://github.com/codeware-sthlm/enjinex","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codeware-sthlm%2Fenjinex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codeware-sthlm%2Fenjinex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codeware-sthlm%2Fenjinex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codeware-sthlm%2Fenjinex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/codeware-sthlm","download_url":"https://codeload.github.com/codeware-sthlm/enjinex/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244130283,"owners_count":20402753,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["certbot","certificate","docker","jest","letsencrypt","managing-certificates","nginx","nodejs","nx-workspace","ssl","typescript","winston-logger"],"created_at":"2024-11-24T05:13:09.080Z","updated_at":"2025-12-31T00:16:43.948Z","avatar_url":"https://github.com/codeware-sthlm.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# enjinex \u003c!-- omit in toc --\u003e\n\n![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/trekkilabs/enjinex?include_prereleases)\n![GitHub top language](https://img.shields.io/github/languages/top/trekkilabs/enjinex)\n![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/trekkilabs/enjinex)\n\n![GitHub Workflow Status](https://img.shields.io/github/workflow/status/trekkilabs/enjinex/ci)\n![CodeQL](https://github.com/trekkilabs/enjinex/workflows/CodeQL/badge.svg)\n[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=trekkilabs_enjinex\u0026metric=alert_status)](https://sonarcloud.io/dashboard?id=trekkilabs_enjinex)\n[![codecov](https://codecov.io/gh/trekkilabs/enjinex/branch/master/graph/badge.svg?token=C1X7B0I4A0)](https://codecov.io/gh/trekkilabs/enjinex)\n\n[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=trekkilabs_enjinex\u0026metric=sqale_rating)](https://sonarcloud.io/dashboard?id=trekkilabs_enjinex)\n[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=trekkilabs_enjinex\u0026metric=reliability_rating)](https://sonarcloud.io/dashboard?id=trekkilabs_enjinex)\n[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=trekkilabs_enjinex\u0026metric=security_rating)](https://sonarcloud.io/dashboard?id=trekkilabs_enjinex)\n\nCreate and automatically renew website SSL certificates using the free [Let's Encrypt](https://letsencrypt.org/) certificate authority, and its client [Certbot](https://certbot.eff.org/), built on top of the [Nginx](https://www.nginx.com/) webserver.\n\n## Features \u003c!-- omit in toc --\u003e\n\n|                                                                               |\n| ----------------------------------------------------------------------------- |\n| Distributed as Docker image                                                   |\n| Built with Node                                                               |\n| Type safe code with TypeScript                                                |\n| [Multi-platform support](#desktop_computer--supported-platforms)              |\n| [Node signal handling](#man_shrugging--how-does-this-work) to prevent zombies |\n| Configure [multiple domains](#multiple-domains)                               |\n| Automatic Let's Encrypt certificate renewal                                   |\n| [Persistent volumes](#persistent-volumes) for certificates and logs           |\n| Monorepo tooling by [Nx](nx.dev)                                              |\n| Unit tests                                                                    |\n| Auto linting                                                                  |\n| [Diffie-Hellman parameters](#diffie-hellman-parameters)                       |\n| **A+** rating on [SSL Labs](https://ssllabs.com)                              |\n| **A+** rating on [Security Headers](https://securityheaders.com)              |\n\n### SSL Labs rating\n\n![alt text][ssl-logo]\n\n[ssl-logo]: /assets/ssl-labs-w800.png 'SSL Labs rating'\n\n### Security Headers rating\n\n![alt text][shr-logo]\n\n[shr-logo]: /assets/security-headers-w850.png 'Security Headers rating'\n\n## Table of contents \u003c!-- omit in toc --\u003e\n\n- [Supported platforms](#supported-platforms)\n- [Usage](#usage)\n- [Domain security](#domain-security)\n- [How does this work?](#how-does-this-work)\n- [Managing certificates](#managing-certificates)\n- [Useful Docker commands](#useful-docker-commands)\n- [Reference sites](#reference-sites)\n- [Acknowledgments](#acknowledgments)\n\n## Supported platforms\n\nDeployed releases can be found on Docker Hub [https://hub.docker.com/r/trekkilabs/enjinex](https://hub.docker.com/r/trekkilabs/enjinex).\n\n| Platform     | Architecture   | Computers                                |\n| ------------ | -------------- | ---------------------------------------- |\n| linux/amd64  | AMD 64-bit x86 | Most today and the default Docker choice |\n| linux/arm64  | ARM 64-bit     | Raspberry Pi 3 _(and later)_             |\n| linux/arm/v7 | ARM 64-bit     | Raspberry Pi 2 Model B                   |\n\n## Usage\n\n### Prerequisites\n\nThe computer using this image must be reached from public for the certificates to be verified and created.\n\nMake sure that your domain name is entered correctly and the DNS A/AAAA record(s) for that domain contain(s) the right IP address. Additionally, check that your computer has a publicly routable IP address and that no firewalls are preventing the server from communicating with the client.\n\n### Environment Variables\n\n#### Required\n\n- `CERTBOT_EMAIL`  \n  Usually the domain owner's email, used by Let's Encrypt as contact email in case of any security issues.\n\n#### Optional\n\n- `NODE_ENV`  \n  For the official image this value is set to `production`, which means all renewal request are sent to Let's Encrypt `production` site. So, any other value e.g. `staging` or `abc` will use the `staging` site.\n\n- `DRY_RUN`  \n  This value is set to `N` by default, which will create real certificates. When this is set to `Y` renewal requests are sent but no changes to the certificate files are made. Use this to test domain setup and prevent any mistakes from creating bad certificates.\n\n- `ISOLATED`  \n  This value is set to `N` by default. When this is set to `Y` the certbot request is never made and status is faked successful. Isolated mode is only valuable during development or test, when your computer isn't setup to receive responses on port 80 and 443. With this option it's still possible to spin up the containter and let the renewal process loop do its thing. [Read about how to run isolated tests.](###run-isolated-tests)\n\n### Persistent Volumes\n\n- `/etc/letsencrypt`  \n  Generated domain certificates stored in domain specific folders.\n\n  _Stored as Docker volume: `letsencrypt_cert`_\n\n- `/etc/nginx/ssl`  \n  Common certificates for all domains, e.g. Diffie-Hellman parameters file.\n\n  _Stored as Docker volume: `ssl`_\n\n- `/var/log/letsencrypt`  \n  Let's Encrypt logs.\n\n  _Stored as Docker volume: `letsencrypt_logs`_\n\n- `/var/log/nginx`  \n  Nginx access and error logs.\n\n  _Stored as Docker volume: `nginx_logs`_\n\n### Domain Configurations\n\nEvery domain to request certificates for must be stored in folder `conf.d`. The file should be named e.g. `domain.com.conf` and contain data at minimum:\n\n```nginx\nserver {\n  listen              443 ssl default_server;\n  server_name         domain.com www.domain.com;\n\n  ssl_certificate     /etc/letsencrypt/live/domain.com/fullchain.pem;\n  ssl_certificate_key /etc/letsencrypt/live/domain.com/privkey.pem;\n\n  include             /etc/nginx/secure.d/header.conf;\n  include             /etc/nginx/secure.d/ssl.conf;\n\n  location / {\n    ...\n  }\n}\n```\n\n\u003e It's very important that the domain name (e.g. `my-site.io`) match for:\n\u003e\n\u003e - File name `my-site.io.conf`\n\u003e\n\u003e - Configuration property `server_name` to be `my-site.io`\n\u003e - Configuration properties\n\u003e   - `ssl_certificate` to be `/etc/letsencrypt/live/my-site.io/fullchain.pem`\n\u003e   - `ssl_certificate_key` to be `/etc/letsencrypt/live/my-site.io/privkey.pem`\n\n#### Multiple domains\n\nIt's possible to store several domains in one certificate. To do this the property `server_name` should contain all certificate domains. Important! All domains must be the same host and the host must be the first domain.\n\n```nginx\nserver {\n  ...\n  server_name  domain.com www.domain.com sub.domain.com;\n  ...\n}\n```\n\n\u003e Using a `server` block that listens on port 80 may cause issues with renewal. This container will already handle forwarding to port 443, so they are unnecessary. See `nginx_conf.d/http.conf`.\n\n### Build and run yourself\n\nIf you have pulled the repository and are experimenting or just whats to build it yourself, the image could be built like this:\n\n```sh\ndocker build -t enjinex:local .\n```\n\nThe command must be executed inside `project/` folder.\n\nPrior to running the image the domains of interest must be created inside `conf.d/` folder. Then the container is launched like this:\n\n```sh\ndocker run -it --rm -d \\\n           -p 80:80 -p 443:443 \\\n           --env CERTBOT_EMAIL=owner@domain.com \\\n           -v \"$(pwd)/conf.d:/etc/nginx/user.conf.d:ro\" \\\n           -v \"$(pwd)/letsencrypt:/etc/letsencrypt\" \\\n           -v \"$(pwd)/nginx:/var/log/nginx\" \\\n           -v \"$(pwd)/ssl:/etc/nginx/ssl\" \\\n           --name enjinex \\\n           enjinex:local\n```\n\n\u003e Here we use local folders for volumes `letsencrypt` and `nginx`, to benefit transparency during testing. For a production like setup this is not recommended.\n\n### Run with `docker-compose`\n\nThere's an official Docker image deployed to GitLab Container Registry that can be used out of the box. The easiest way is to create a `docker-compose.yml` file like this:\n\n```yml\nversion: '3.8'\n\nservices:\n  enjinex:\n    image: trekkilabs/enjinex:latest\n    restart: unless-stopped\n    environment:\n      CERTBOT_EMAIL: owner@domain.com\n    ports:\n      - '80:80'\n      - '443:443'\n    volumes:\n      - ./conf.d:/etc/nginx/user.conf.d:ro\n      - letsencrypt_cert:/etc/letsencrypt\n      - letsencrypt_logs:/var/log/letsencrypt\n      - nginx_logs:/var/log/nginx\n      - ssl:/etc/nginx/ssl\n\nvolumes:\n  letsencrypt_cert:\n  letsencrypt_logs:\n  nginx_logs:\n  ssl:\n```\n\nThen pull the image, build and start the container:\n\n```sh\ndocker-compose build --pull\ndocker-compose -d up\n```\n\n### Run isolated tests\n\nIsolated test are used when the computer can not receive reponses from Let's Encrypt. Mostly this is your local development computer.\n\nDuring these tests no requests are sent to Let's Encrypt but the process is otherwise the real one. By running isolated tests the developer can see the output of the latest changes and get a quick sanity check as a complement to unit tests.\n\nThe only problem is the certificates provided by Let's Encrypt and this connection is, described above, disconnected. Luckily there's a script creating self signed certificate files.\n\n```sh\n./isolated-test/make-certs.sh\n```\n\n```sh\ndocker-compose up\n```\n\nA fake domain `localhost` is prepared in folder `isolated-test` but there's nothing stopping from creating more fake domains. Just create certificates from those domains as well, e.g. `my-site.com`.\n\n```sh\n./isolated-test/make-certs.sh my-site.com\n```\n\n### Run test with expected failure\n\nThis test is a variant of isolated test with the same configuration. The only difference is that the renewal request is actually sent to Let's Encrypt but with `--dry-run` flag applied. However we know that `localhost` isn't a fully qualified domain and hence the request will fail.\n\nIt's an educational example how `stderr` from a spawned `certbot` command may look like.\n\n```sh\ndocker-compose -f docker-compose.dry-run.yml up\n```\n\n## Domain security\n\n### Image provided configuration\n\nSome configurations are provided by the image. Those files are located in the `nginx_conf.d/secure.d` folder.\n\n- `header.conf`  \n  This file contains header properties to fine tune the browser security and availability behaviour. Test the settings on [Security Headers](https://securityheaders.com/).\n\n  More about headers on site [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/), or use the links provided inside `header.conf` file.\n\n  \u003e It's highly likely that these properties needs to be changed depending on your, or the hosted sites needs. Especially _Content Security Policy_ could lead to a site with a lot of console errors.\n\n- `location.conf`  \n  This file is not used by default by the image but is available for [reverse proxy location blocks](http://nginx.org/en/docs/http/ngx_http_upstream_module.html).\n  There's an example inside this file.\n\n- `ssl.conf`  \n  This file contains security properties including Diffie-Hellman parameters.\n\n### Diffie-Hellman parameters\n\nThis adds another layer of security. It's best explained by [Wikipedia](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange).\n\nThe default configuration promotes 2048 bits. Higher bit rates could be used but this will lead to reduced performance. It's your choice but 2048 bits is quite hard to crack.\n\n## How does this work?\n\n### Node service\n\nInstead of starting a shell script, which is very common, this solution starts a Node service. The reason for this is to have more control over development, mostly regarding unit test, but also to benefit from TypeScript. [TypeScript](https://www.typescriptlang.org/) is a superset of JavaScript and provides type-safe code.\n\nThe service is starter within the image, declared in `Dockerfile`.\n\n```dockerfile\n...\nENTRYPOINT [\"node\", \"/app/dist/apps/init/main.js\"]\n...\n```\n\nThe `init` application is the main container thread and will always get PID 1. All other processes spawned by `init` will be child processes of `init`. It's therefore important for `init` to setup listeners for `SIG`-signals to prevent the child processes to become zoombies in case `init` gets terminated.\n\nThe flow chart for `init` application:\n\n1. Setup listeners to `SIGINT`, `SIGTERM` and `SIGUSR2`.\n\n2. Look for Diffie-Hellman parameters file. Create the file if wasn't found.  \n   Exit `init` if `/etc/nginx/ssl/dhparam.pem` could not be created.\n3. Transfer user domain configurations to `Nginx` configuration folder.  \n   `(local machine repo):conf.d/*.conf` :arrow_right: `(container):/etc/nginx/conf.d/`\n4. Analyze all domain configuration files and make sure all certificate `.pem` files exists. When one is missing the file is renamed with a `.pending` suffix. If we don't do this and start `Nginx` the domain is started in a insecure state.\n5. Test `Nginx` configuration and exit if failed.\n6. Start `Nginx` service by spawning a new child process. Setup listeners to `close`, `stdout`, `stderr`, `disconnect` and `error` events. All events output log data but only the `close` event will sent a exit signal to the parent process.\n7. Start the main loop by creating a interval timer. Default value of timer is 24 hours.\n\n   1. Begin the renewal process for all valid domains.\n\n   2. Exit if environment `CERTBOT_EMAIL` is undefined.\n   3. Get all [valid domains](#valid-domain-checks) from configuration files.\n   4. For each domain send renewal request to Let's Encrypt and let they determine if the certificate needs to be renewed. Otherwise all `.pem` files are left unchanged.\n   5. If the request fails all processes will be terminated. But when successful and the domain was marked by a `.pending` suffix, it will be renamed back to the origin name.\n   6. Reload `Nginx` configuration after all the domains have been processed. This is to ensure that the pending domains gets activated.\n\n8. Wait for the timer to elapse and another renewal process will start.\n\n### `Nginx` configuration\n\nInside folder `nginx_conf.d` there are some configuration files for `Nginx` that works out of the box. It's not intended for those to be edited but, of course, if you know what you're doing feel free to improve or adjust to your needs.\n\n- Local folder: `nginx_conf.d/...`\n\n- Container folder: `/etc/nginx/...`\n\n| Config file    | Local folder | Container folder | Responsibility                                               |\n| -------------- | ------------ | ---------------- | ------------------------------------------------------------ |\n| `certbot.conf` | `conf.d/`    | `conf.d/`        | Verifying ACME challenges from Let's Encrypty                |\n| `gzip.conf`    | `conf.d/`    | `conf.d/`        | Comression (`gzip`) settings                                 |\n| `http.conf`    | `conf.d/`    | `conf.d/`        | Port 80 listener; redirects to `certbot.conf` or 443 (https) |\n| `header.conf`  | `secure.d/`  | `secure.d/`      | Header properties for improved security                      |\n| `ssl.conf`     | `secure.d/`  | `secure.d/`      | SSL/TLS properties for strong encryption                     |\n\n### User domain configuration\n\nIt's no purpose to run the image if no domains are specified. All the user domains should be located inside `conf.d/` folder. **Not** the one `nginx_conf.d/conf.d/` described obove, but a new folder that needs to be created. Example of a domain configuration is described in [domain configurations](#domain-configurations). A practical use case is also available in `isolated_test/` folder, which is described in [run isolated tests](#run-isolated-tests).\n\nCreate as many files as needed where each file will create a certificate. E.g. a certificate for `my-site.io` requires a file named `my-site.io.conf`.\n\n#### Valid domain checks\n\nFor a domain to be marked as _valid_ and hence be included in the renewal process, a number of checks needs to pass:\n\n1. The domain [extracted from configuration](#domain-configurations) file must be a valid host.  \n   It's also possible to use `localhost`, but that is actually only useful when running isolated test.\n\n2. Property `ssl_certificate_key` must exist inside configuration file and the path to the certificate file must [correspond to the domain name](#domain-configurations).\n3. For property `server_name` inside the configuration file\n   1. the primary domain (e.g. `my-site.io`) must be ordered first\n   2. all domains must belong to the same host\n   3. all domains must be unique\n\n## Managing certificates\n\nCertificates can also be accessed from the running container by manually executing the `certbot` command.\n\nMore commands can be found in [References](#bookmark--reference-sites).\n\n### List certificates known by `certbot`\n\nList all certificates\n\n```sh\ndocker exec enjinex certbot certificates\n```\n\nor just domain.com\n\n```sh\ndocker exec enjinex certbot certificates --cert-name domain.com\n```\n\n### Revoke a certificate\n\n```sh\ndocker exec enjinex certbot revoke --cert-path /etc/letsencrypt/live/domain.com/fullchain.pem\n```\n\nThen delete all certificate files.\n\n```sh\ndocker exec enjinex certbot delete --cert-name domain.com --non-interactive\n```\n\n### Force renewal of certificates\n\nThis feature uses `SIGUSR2` to notify the container to start a renewal process with `--force-renewal` flag applied.\n\n```sh\ndocker kill --signal=USR2 enjinex\n```\n\nBut don't do this to often, otherwise the Let's Encrypt limit might be reached.\n\n## Useful Docker commands\n\n### Running containers\n\n```sh\ndocker ps\n```\n\n### Container logs\n\n`enjinex` can be found using the previous command.\n\n```sh\n# Follow log output run-time\ndocker logs -f enjinex\n\n# Display last 50 rows\ndocker logs -n 50 enjinex\n```\n\nThese logs are also saved by `winston` as JSON objects to `/logs` folder.\n\n```sh\n# Error logs\ndocker exec enjinex tail -200f /logs/error.log\n\n# All other log level\ndocker exec enjinex tail -200f /logs/combined.log\n```\n\n### Get a shell to the container\n\n```sh\ndocker container exec -it enjinex /bin/bash\n```\n\n### List all `Let's Encrypt` domain folders\n\n```sh\ndocker exec enjinex ls -la /etc/letsencrypt/live\n```\n\n### List secret files for domain `domain.com`\n\n```sh\ndocker exec enjinex ls -la /etc/letsencrypt/live/domain.com\n```\n\n### Display `Nginx` main configuration\n\n```sh\ndocker exec enjinex cat /etc/nginx/nginx.conf\n```\n\n### List read-only `Nginx` configuration files provided by `enjinex` image\n\n```sh\n# http/https configuration\ndocker exec enjinex ls -la /etc/nginx/conf.d\n\n# Secure server\ndocker exec enjinex ls -la /etc/nginx/secure.d\n```\n\n### Follow `Nginx` logs\n\n```sh\n# Access logs\ndocker exec enjinex tail -200f /var/log/nginx/access.log\n\n# Error logs\ndocker exec enjinex tail -200f /var/log/nginx/error.log\n```\n\n## Reference sites\n\n- [Let's Encrypt](https://letsencrypt.org/)\n- [Certbot](https://certbot.eff.org/)\n- [Managing certificates](https://certbot.eff.org/docs/using.html?highlight=hook#managing-certificates)\n- [GitHub Actions using Docker buildx](https://github.com/marketplace/actions/build-and-push-docker-images#usage)\n\n## Acknowledgments\n\nThis repository was originally cloned from `@staticfloat`, kudos to him and all other contributors. The reason to make a clone is to convert from `bash` to `TypeScript` and privde unit tests. Still many good ideas are kept but in a different form.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodeware-sthlm%2Fenjinex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcodeware-sthlm%2Fenjinex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodeware-sthlm%2Fenjinex/lists"}