Full Code of nginx-proxy/acme-companion for AI

main a79d2b7dca55 cached
87 files
247.1 KB
71.0k tokens
1 requests
Download .txt
Showing preview only (268K chars total). Download the full file or copy to clipboard to get everything.
Repository: nginx-proxy/acme-companion
Branch: main
Commit: a79d2b7dca55
Files: 87
Total size: 247.1 KB

Directory structure:
gitextract_zsr4to63/

├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── bug_report.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── build-publish.yml
│       ├── dockerhub-description.yml
│       └── test.yml
├── .gitignore
├── .shellcheckrc
├── Dockerfile
├── LICENSE
├── README.md
├── app/
│   ├── cert_status
│   ├── cleanup_test_artifacts
│   ├── dhparam/
│   │   ├── ffdhe2048.pem
│   │   ├── ffdhe3072.pem
│   │   └── ffdhe4096.pem
│   ├── dhparam.pem.default
│   ├── entrypoint.sh
│   ├── force_renew
│   ├── functions.sh
│   ├── letsencrypt_service
│   ├── letsencrypt_service_data.tmpl
│   ├── nginx_location.conf
│   ├── signal_le_service
│   └── start.sh
├── docs/
│   ├── Advanced-usage.md
│   ├── Basic-usage.md
│   ├── Container-configuration.md
│   ├── Container-utilities.md
│   ├── Docker-Compose.md
│   ├── Getting-containers-IDs.md
│   ├── Google-Trust-Services.md
│   ├── Hooks.md
│   ├── Invalid-authorizations.md
│   ├── Let's-Encrypt-and-ACME.md
│   ├── Persistent-data.md
│   ├── README.md
│   ├── Standalone-certificates.md
│   └── Zero-SSL.md
├── install_acme.sh
└── test/
    ├── README.md
    ├── config.sh
    ├── github_actions/
    │   └── containers-logs.sh
    ├── run.sh
    ├── setup/
    │   ├── pebble/
    │   │   ├── compose.yaml
    │   │   ├── pebble-config-eab.json
    │   │   ├── pebble-config.json
    │   │   └── setup-pebble.sh
    │   ├── setup-boulder.sh
    │   ├── setup-local.sh
    │   └── setup-nginx-proxy.sh
    └── tests/
        ├── acme_accounts/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── acme_eab/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── acme_hooks/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── certs_default_renew/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── certs_san/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── certs_single/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── certs_single_domain/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── certs_standalone/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── container_restart/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── docker_api/
        │   └── run.sh
        ├── docker_api_legacy/
        │   └── run.sh
        ├── force_renew/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── location_config/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── ocsp_must_staple/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── permissions_custom/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── permissions_default/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── private_keys/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── symlinks/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── test-functions.sh
        └── unit_tests/
            └── expected-std-out.txt

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
+.*
+docs
+go
+test
+README.md
+schema.png


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

If your are using the latest image tag and recently updated your image: please make sure you've checked the required read on the project's README.

HTTPS does not work / certificate aren't created : please check in your acme-companion container logs if an authorization or verify error is mentioned, if it is please do the following before opening an issue:
- check and follow the troubleshooting instructions in the docs.
- search the existing similar issues, both opened and closed.

Bug description
-----------------

A clear and concise description of what the bug is.

acme-companion image version
-----------------

Please provide the container version that should be printed to the first line of log at container startup:
```
Info: running acme-companion version v2.0.0
```

If this first log line isn't present you are using a v1 image: please provide the tagged version you are using. If you are not using a tagged version latest, please try again with a tagged release before opening an issue (the last v1 tagged release is v1.13.1).

nginx-proxy's Docker configuration
-----------------

Please provide the configuration (either command line, compose file, or other) of your nginx-proxy stack and your proxied container(s).

You can obfuscate information you want to keep private (and should obfuscate configuration secrets) such as domain(s) and/or email adress(es), but other than that please provide the full configurations and not the just snippets of the parts that seem relevants to you.

rendered nginx configuration
-----------------

Please provide the rendered nginx configuration:

```console
docker exec name-of-the-nginx-container nginx -T
```

Containers logs
-----------------

Please provide the logs of:
- your acme-companion container
- your nginx-proxy container (or nginx and docker-gen container in a three containers setup)

```console
docker logs name-of-the-companion-container
```

Docker host
-----------------

 - OS: [e.g. Ubuntu 20.04]
 - Docker version: output of `docker version`


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  # Maintain dependencies for Docker
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "daily"
    commit-message:
      prefix: "build"
    labels:
      - "type/build"

  # Maintain GitHub Actions
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    commit-message:
      prefix: "ci"
    labels:
      - "type/ci"


================================================
FILE: .github/workflows/build-publish.yml
================================================
name: Build and publish Docker image

permissions:
  contents: read
  packages: write

on:
  workflow_dispatch:
  schedule:
    - cron: "0 0 * * 1"
  push:
    branches:
      - main
      - dev
      - stable
    tags:
      - "v*.*.*"
    paths:
      - ".dockerignore"
      - ".github/workflows/build-publish.yml"
      - "app/*"
      - "Dockerfile"
      - "install_acme.sh"

jobs:
  multiarch-build:
    name: Build and publish Docker image
    runs-on: ubuntu-latest
    if: (github.event_name == 'schedule' && github.repository == 'nginx-proxy/acme-companion') || (github.event_name != 'schedule')
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Retrieve version
        id: acme-companion_version
        run: echo "VERSION=$(git describe --tags)" >> "$GITHUB_OUTPUT"

      - name: Get Docker tags
        id: docker_meta
        uses: docker/metadata-action@v5
        with:
          images: |
            ghcr.io/nginx-proxy/acme-companion
            nginxproxy/acme-companion
            jrcs/letsencrypt-nginx-proxy-companion
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=raw,value=latest,enable={{is_default_branch}}
          labels: |
            org.opencontainers.image.authors=Nicolas Duchon <nicolas.duchon@gmail.com> (@buchdag), Yves Blusseau
            org.opencontainers.image.version=${{ steps.acme-companion_version.outputs.VERSION }}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4

      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v7
        with:
          context: .
          build-args: GIT_DESCRIBE=${{ steps.acme-companion_version.outputs.VERSION }}
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          sbom: true
          push: true
          provenance: mode=max
          tags: ${{ steps.docker_meta.outputs.tags }}
          labels: ${{ steps.docker_meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Image digest
        run: echo ${{ steps.docker_build.outputs.digest }}


================================================
FILE: .github/workflows/dockerhub-description.yml
================================================
name: Update Docker Hub Description

permissions:
  contents: read

on:
  push:
    branches:
      - main
    paths:
      - README.md
      - .github/workflows/dockerhub-description.yml

jobs:
  dockerHubDescription:
    name: Update Docker Hub Description
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Docker Hub Description
        uses: peter-evans/dockerhub-description@v5
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN_RWD }}
          repository: nginxproxy/acme-companion
          short-description: ${{ github.event.repository.description }}
          enable-url-completion: true


================================================
FILE: .github/workflows/test.yml
================================================
name: Tests

permissions:
  contents: read

on:
  workflow_dispatch:
  push:
    branches:
      - main
    paths-ignore:
      - "docs/**"
      - "*.md"
  pull_request:
    paths-ignore:
      - "docs/**"
      - "*.md"

env:
  ACME_CA: pebble
  DOCKER_GEN_CONTAINER_NAME: nginx-proxy-gen
  IMAGE: nginxproxy/acme-companion
  NGINX_CONTAINER_NAME: nginx-proxy
  TEST_DOMAINS: le1.wtf,le2.wtf,le3.wtf

jobs:
  companion-build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Code
        uses: actions/checkout@v6

      - name: Build Image
        run: docker build -t "$IMAGE" .

      - name: Inspect Image
        run: docker inspect "$IMAGE"

      - name: Get acme.sh Version
        run: docker run --rm "$IMAGE" acme.sh --version

      - name: List Docker Images
        run: docker images

      - name: Export Image Artifact
        run: docker save $IMAGE > companion.tar

      - name: Upload Image Artifact
        uses: actions/upload-artifact@v7
        with:
          name: companion.tar
          path: companion.tar

  docker-specs-tests:
    needs: companion-build
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Docker official images tests
        uses: actions/checkout@v6
        with:
          repository: docker-library/official-images
          path: official-images

      - name: Download Builded Image
        uses: actions/download-artifact@v8
        with:
          name: companion.tar

      - name: Import Builded Image
        run: docker load < companion.tar

      - name: Docker Specifications Testing
        run: official-images/test/run.sh "$IMAGE"

      - name: Display containers logs
        if: ${{ failure() }}
        run: test/github_actions/containers-logs.sh

  integration-tests:
    needs:
      - companion-build
    strategy:
      fail-fast: false
      matrix:
        test-name:
          [
            docker_api,
            docker_api_legacy,
            location_config,
            certs_single,
            certs_san,
            certs_single_domain,
            certs_standalone,
            force_renew,
            acme_accounts,
            private_keys,
            container_restart,
            permissions_default,
            permissions_custom,
            symlinks,
            acme_hooks,
            ocsp_must_staple,
          ]
        setup: [2containers, 3containers]
        pebble-config: [pebble-config.json]
        include:
          - test-name: acme_eab
            setup: 2containers
            pebble-config: pebble-config-eab.json
          - test-name: acme_eab
            setup: 3containers
            pebble-config: pebble-config-eab.json
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Code
        uses: actions/checkout@v6

      # PREPARE RUNNER ENV
      - name: Add Test Domains in /etc/hosts
        run: |
          IFS=',' read -r -a test_domains <<< "$TEST_DOMAINS"
          test_domains+=(pebble pebble-challtestsrv)
          for domain in "${test_domains[@]}"
          do
              echo "127.0.0.1 $domain" | sudo tee -a /etc/hosts
          done

      - name: Setup Pebble
        env:
          PEBBLE_CONFIG: ${{ matrix.pebble-config }}
        run: test/setup/pebble/setup-pebble.sh

      - name: Setup nginx-proxy
        env:
          SETUP: ${{ matrix.setup }}
        run: test/setup/setup-nginx-proxy.sh

      # ADD BUILT IMAGE
      - name: Download Built Image
        uses: actions/download-artifact@v8
        with:
          name: companion.tar

      - name: Import Built Image
        run: docker load < companion.tar

      # TEST
      - name: Integration Testing
        env:
          SETUP: ${{ matrix.setup }}
          PEBBLE_CONFIG: ${{ matrix.pebble-config }}
        run: test/run.sh -t ${{ matrix.test-name }} "$IMAGE"

      - name: Display containers logs
        if: ${{ failure() }}
        env:
          SETUP: ${{ matrix.setup }}
        run: test/github_actions/containers-logs.sh


================================================
FILE: .gitignore
================================================
.docker
.vscode
certs/
conf.d/
data/
vhost.d/

# tests
go/
nginx.tmpl
pebble.minica.pem
test/local_test_env.sh
test/tests/docker_api/expected-std-out.txt
test/tests/container_restart/docker_event_out.txt
test/tests/certs_standalone/letsencrypt_user_data
test/tests/location_config/le2.wtf


================================================
FILE: .shellcheckrc
================================================
external-sources=true


================================================
FILE: Dockerfile
================================================
# syntax=docker/dockerfile:1
FROM docker.io/nginxproxy/docker-gen:0.16.2 AS docker-gen

FROM docker.io/library/alpine:3.23.3

ARG GIT_DESCRIBE="unknown"
ARG ACMESH_VERSION=3.1.2

ENV ACMESH_VERSION=${ACMESH_VERSION} \
    COMPANION_VERSION=${GIT_DESCRIBE} \
    DOCKER_HOST=unix:///var/run/docker.sock \
    PATH=${PATH}:/app

# Install packages required by the image
RUN apk add --no-cache --virtual .bin-deps \
    bash \
    bind-tools \
    coreutils \
    curl \
    jq \
    libidn \
    oath-toolkit-oathtool \
    openssh-client \
    openssl \
    sed \
    socat \
    tar \
    tzdata

# Install docker-gen from the nginxproxy/docker-gen image
COPY --from=docker-gen /usr/local/bin/docker-gen /usr/local/bin/

# Install acme.sh
COPY /install_acme.sh /app/install_acme.sh
RUN chmod +rx /app/install_acme.sh \
    && sync \
    && /app/install_acme.sh \
    && rm -f /app/install_acme.sh

COPY app LICENSE /app/

WORKDIR /app

ENTRYPOINT [ "/bin/bash", "/app/entrypoint.sh" ]
CMD [ "/bin/bash", "/app/start.sh" ]


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2015-2017 Yves Blusseau
Copyright (c) 2017-2022 Nicolas Duchon

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
[![Tests](https://github.com/nginx-proxy/acme-companion/actions/workflows/test.yml/badge.svg)](https://github.com/nginx-proxy/acme-companion/actions/workflows/test.yml)
[![GitHub release](https://img.shields.io/github/release/nginx-proxy/acme-companion.svg)](https://github.com/nginx-proxy/acme-companion/releases)
[![Docker Image Size](https://img.shields.io/docker/image-size/nginxproxy/acme-companion?sort=semver)](https://hub.docker.com/r/nginxproxy/acme-companion "Click to view the image on Docker Hub")
[![Docker stars](https://img.shields.io/docker/stars/nginxproxy/acme-companion.svg)](https://hub.docker.com/r/nginxproxy/acme-companion "Click to view the image on Docker Hub")
[![Docker pulls](https://img.shields.io/docker/pulls/nginxproxy/acme-companion.svg)](https://hub.docker.com/r/nginxproxy/acme-companion "Click to view the image on Docker Hub")

**acme-companion** is a lightweight companion container for [**nginx-proxy**](https://github.com/nginx-proxy/nginx-proxy).

It handles the automated creation, renewal and use of SSL certificates for proxied Docker containers through the ACME protocol.

### Features:
* Automated creation/renewal of Let's Encrypt (or other ACME CAs) certificates using [**acme.sh**](https://github.com/acmesh-official/acme.sh).
* Let's Encrypt / ACME domain validation through `HTTP-01` (by default) or [`DNS-01`](https://github.com/nginx-proxy/acme-companion/blob/main/docs/Let's-Encrypt-and-ACME.md#dns-01-acme-challenge) challenge.
* Automated update and reload of nginx config on certificate creation/renewal.
* Support creation of [Multi-Domain (SAN) Certificates](https://github.com/nginx-proxy/acme-companion/blob/main/docs/Let's-Encrypt-and-ACME.md#multi-domains-certificates).
* Support creation of [Wildcard Certificates](https://community.letsencrypt.org/t/acme-v2-production-environment-wildcards/55578) (with `DNS-01` challenge only).
* Creation of a strong [RFC7919 Diffie-Hellman Group](https://datatracker.ietf.org/doc/html/rfc7919#appendix-A) at startup.
* Work with all versions of docker.

### HTTP-01 challenge requirements:
* Your host **must** be publicly reachable on **both** port [`80`](https://letsencrypt.org/docs/allow-port-80/) and [`443`](https://github.com/nginx-proxy/acme-companion/discussions/873#discussioncomment-1410225).
* Check your firewall rules and [**do not attempt to block port `80`**](https://letsencrypt.org/docs/allow-port-80/) as that will prevent `HTTP-01` challenges from completing.
* For the same reason, you can't use nginx-proxy's [`HTTPS_METHOD=nohttp`](https://github.com/nginx-proxy/nginx-proxy#how-ssl-support-works).
* The (sub)domains you want to issue certificates for must correctly resolve to the host.
* If your (sub)domains have AAAA records set, the host must be publicly reachable over IPv6 on port `80` and `443`.

If you can't meet these requirements, you can use the `DNS-01` challenge instead. Please refer to the [documentation](https://github.com/nginx-proxy/acme-companion/blob/main/docs/Let's-Encrypt-and-ACME.md#dns-01-acme-challenge) for more information.

In addition to the above, please ensure that your DNS provider answers correctly to CAA record requests. [If your DNS provider answer with an error, Let's Encrypt won't issue a certificate for your domain](https://letsencrypt.org/docs/caa/). Let's Encrypt do not require that you set a CAA record on your domain, just that your DNS provider answers correctly.

![schema](https://github.com/nginx-proxy/acme-companion/blob/main/schema.png)

## Basic usage (with the nginx-proxy container)

Two writable volumes must be declared on the **nginx-proxy** container so that they can be shared with the **acme-companion** container:

* `/etc/nginx/certs` to store certificates and private keys (readonly for the **nginx-proxy** container).
* `/usr/share/nginx/html` to write `http-01` challenge files.

Additionally, a third volume must be declared on the **acme-companion** container to store `acme.sh` configuration and state: `/etc/acme.sh`.

Please also read the doc about [data persistence](./docs/Persistent-data.md).

Example of use:

### Step 1 - nginx-proxy

Start **nginx-proxy** with the two additional volumes declared:

```shell
$ docker run --detach \
    --name nginx-proxy \
    --publish 80:80 \
    --publish 443:443 \
    --volume certs:/etc/nginx/certs \
    --volume html:/usr/share/nginx/html \
    --volume /var/run/docker.sock:/tmp/docker.sock:ro \
    nginxproxy/nginx-proxy
```

Binding the host docker socket (`/var/run/docker.sock`) inside the container to `/tmp/docker.sock` is a requirement of **nginx-proxy**.

### Step 2 - acme-companion

Start the **acme-companion** container, getting the volumes from **nginx-proxy** with `--volumes-from`:

```shell
$ docker run --detach \
    --name nginx-proxy-acme \
    --volumes-from nginx-proxy \
    --volume /var/run/docker.sock:/var/run/docker.sock:ro \
    --volume acme:/etc/acme.sh \
    --env "DEFAULT_EMAIL=mail@yourdomain.tld" \
    nginxproxy/acme-companion
```

The host docker socket has to be bound inside this container too, this time to `/var/run/docker.sock`.

Albeit **optional**, it is **recommended** to provide a valid default email address through the `DEFAULT_EMAIL` environment variable, so that Let's Encrypt can warn you about expiring certificates and allow you to recover your account.

### Step 3 - proxied container(s)

Once both **nginx-proxy** and **acme-companion** containers are up and running, start any container you want proxied with environment variables `VIRTUAL_HOST` and `LETSENCRYPT_HOST` both set to the domain(s) your proxied container is going to use.

[`VIRTUAL_HOST`](https://github.com/nginx-proxy/nginx-proxy#usage) control proxying by **nginx-proxy** and `LETSENCRYPT_HOST` control certificate creation and SSL enabling by **acme-companion**.

Certificates will only be issued for containers that have both `VIRTUAL_HOST` and `LETSENCRYPT_HOST` variables set to domain(s) that correctly resolve to the host, provided the host is publicly reachable.

```shell
$ docker run --detach \
    --name your-proxied-app \
    --env "VIRTUAL_HOST=subdomain.yourdomain.tld" \
    --env "LETSENCRYPT_HOST=subdomain.yourdomain.tld" \
    nginx
```

The containers being proxied must expose the port to be proxied, either by using the `EXPOSE` directive in their Dockerfile or by using the `--expose` flag to `docker run` or `docker create`.

If the proxied container listen on and expose another port than the default `80`, you can force **nginx-proxy** to use this port with the [`VIRTUAL_PORT`](https://github.com/nginx-proxy/nginx-proxy#multiple-ports) environment variable.

Example using [Grafana](https://hub.docker.com/r/grafana/grafana/) (expose and listen on port 3000):

```shell
$ docker run --detach \
    --name grafana \
    --env "VIRTUAL_HOST=othersubdomain.yourdomain.tld" \
    --env "VIRTUAL_PORT=3000" \
    --env "LETSENCRYPT_HOST=othersubdomain.yourdomain.tld" \
    --env "LETSENCRYPT_EMAIL=mail@yourdomain.tld" \
    grafana/grafana
```

Repeat [Step 3](#step-3---proxied-containers) for any other container you want to proxy.

## Additional documentation

Please check the [docs section](https://github.com/nginx-proxy/acme-companion/tree/main/docs).


================================================
FILE: app/cert_status
================================================
#!/bin/bash
function print_cert_info {
  local enddate
  local subject
  local san_str

  # Get the wanted informations with OpenSSL.
  issuer="$(openssl x509 -noout -issuer -in "$1" | sed -n 's/.*CN = \(.*\)/\1/p')"
  enddate="$(openssl x509 -noout -enddate -in "$1" | sed -n 's/notAfter=\(.*$\)/\1/p')"
  subject="$(openssl x509 -noout -subject -in "$1" | sed -n 's/.*CN = \([a-z0-9.-]*\)/- \1/p')"
  san_str="$(openssl x509 -text -in "$1" | grep 'DNS:')"

  case "$issuer" in
    R3 | R4 | E1 | E2)
      issuer="Let's Encrypt $issuer"
      ;;

    *)
      ;;
  esac

  echo "Certificate was issued by $issuer"
  if [[ "$2" == "expired" ]]; then
      echo "Certificate was valid until $enddate"
  else
      echo "Certificate is valid until $enddate"
  fi
  echo "Subject Name:"
  echo "$subject"

  # Display the SAN info only if there is more than one SAN domain.
  while IFS=',' read -ra SAN; do
      if [[ ${#SAN[@]} -gt 1 ]]; then
          echo "Subject Alternative Name:"
          for domain in "${SAN[@]}"; do
              echo "$domain" | sed -n 's/.*DNS:\([a-z0-9.-]*\)/- \1/p'
          done
      fi
  done <<< "$san_str"
}

echo '##### Certificate status #####'
for cert in /etc/nginx/certs/*/fullchain.pem; do
    [[ -e "$cert" ]] || continue
    if [[ -e "${cert%fullchain.pem}chain.pem" ]]; then
        # Verify the certificate with OpenSSL.
        if verify=$(openssl verify -untrusted "${cert%fullchain.pem}chain.pem" "$cert" 2>&1); then
            echo "$verify"
            # Print certificate info.
            print_cert_info "$cert"
        else
            echo "${cert}: EXPIRED"
            # Print certificate info.
            print_cert_info "$cert" "expired"
        fi
    else
        echo "${cert}: no corresponding chain.pem file, unable to verify certificate"
        # Print certificate info.
        print_cert_info "$cert"
    fi

    # Find the .crt files in /etc/nginx/certs which are
    # symlinks pointing to the current certificate.
    unset symlinked_domains
    for symlink in /etc/nginx/certs/*.crt; do
        [[ -e "$symlink" ]] || continue
        if [[ "$(readlink -f "$symlink")" == "$cert" ]]; then
            domain="${symlink%.crt}"
            domain="${domain//\/etc\/nginx\/certs\//}"
            symlinked_domains+=("$domain")
        fi
    done

    # Display symlinks pointing to the current cert if there is any.
    if [[ ${#symlinked_domains[@]} -gt 0 ]]; then
        echo "Certificate is used by the following domain(s):"
        for domain in "${symlinked_domains[@]}"; do
          echo "- $domain"
        done
    fi

    echo '##############################'
done


================================================
FILE: app/cleanup_test_artifacts
================================================
#!/bin/bash

# This script should not be run outside of a test container
[[ "$TEST_MODE" == 'true' ]] || exit 1

while [[ $# -gt 0 ]]; do
    flag="$1"

    case $flag in
        --location-config)
        for domain in 'le1.wtf' '*.example.com' 'test.*' 'le3.pizza' 'subdomain.example.com' 'test.domain.tld'; do
            [[ -f "/etc/nginx/vhost.d/$domain" ]] && rm -f "/etc/nginx/vhost.d/$domain"
        done
        shift
        ;;

        *) #Unknown option
        shift
        ;;
    esac
done

for domain in le1.wtf le2.wtf le3.wtf le4.wtf lim.it; do
    folder="/etc/nginx/certs/$domain"
    [[ -d "$folder" ]] && rm -rf "$folder"
    folder="/etc/acme.sh/default/$domain"
    [[ -d "$folder" ]] && rm -rf "$folder"
    folder="/etc/acme.sh/default/${domain}_ecc"
    [[ -d "$folder" ]] && rm -rf "$folder"
    location_file="/etc/nginx/vhost.d/$domain"
    [[ -f "$location_file" ]] && rm -rf "$location_file" 2> /dev/null
    for extension in key crt chain.pem dhparam.pem; do
        symlink="/etc/nginx/certs/${domain}.${extension}"
        [[ -L "$symlink" ]] && rm -rf "$symlink"
    done
done

exit 0


================================================
FILE: app/dhparam/ffdhe2048.pem
================================================
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
-----END DH PARAMETERS-----


================================================
FILE: app/dhparam/ffdhe3072.pem
================================================
-----BEGIN DH PARAMETERS-----
MIIBiAKCAYEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
ssbzSibBsu/6iGtCOGEfz9zeNVs7ZRkDW7w09N75nAI4YbRvydbmyQd62R0mkff3
7lmMsPrBhtkcrv4TCYUTknC0EwyTvEN5RPT9RFLi103TZPLiHnH1S/9croKrnJ32
nuhtK8UiNjoNq8Uhl5sN6todv5pC1cRITgq80Gv6U93vPBsg7j/VnXwl5B0rZsYu
N///////////AgEC
-----END DH PARAMETERS-----


================================================
FILE: app/dhparam/ffdhe4096.pem
================================================
-----BEGIN DH PARAMETERS-----
MIICCAKCAgEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
ssbzSibBsu/6iGtCOGEfz9zeNVs7ZRkDW7w09N75nAI4YbRvydbmyQd62R0mkff3
7lmMsPrBhtkcrv4TCYUTknC0EwyTvEN5RPT9RFLi103TZPLiHnH1S/9croKrnJ32
nuhtK8UiNjoNq8Uhl5sN6todv5pC1cRITgq80Gv6U93vPBsg7j/VnXwl5B0rZp4e
8W5vUsMWTfT7eTDp5OWIV7asfV9C1p9tGHdjzx1VA0AEh/VbpX4xzHpxNciG77Qx
iu1qHgEtnmgyqQdgCpGBMMRtx3j5ca0AOAkpmaMzy4t6Gh25PXFAADwqTs6p+Y0K
zAqCkc3OyX3Pjsm1Wn+IpGtNtahR9EGC4caKAH5eZV9q//////////8CAQI=
-----END DH PARAMETERS-----


================================================
FILE: app/dhparam.pem.default
================================================
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEAwpR+yYapElMV4DiO+BwKK2N8Ur4giZtga+dslyDMuhY+U4t/97Eq
gdFg2RD5nqrgWCRWEYcbh1kPBOAPWXZ4+N8mZL8pJXaNi2XFA8IxQex283Sz7CX+
qr/zb+piJLx+/6JB/NNTZtKurM3ZQgwdGqSHqeWgvRIgCQAykC1oz7muCsev1IMc
rLig1kyvhg3L1t+uKYV0OtiXONmPglPm9pXRqMQ53Rg/D3CpUpyyTSugOFjVhLrP
Ow+kO6qXBQSDhrL2L0UjprbcVMPHv9bFmWNoTCtC8OYA1OuiA368PWhgeH/76Yu8
4an6/vt3HowDZHKfB3Vb1VwTI+k6hzwhkwIBAg==
-----END DH PARAMETERS-----


================================================
FILE: app/entrypoint.sh
================================================
#!/bin/bash

set -u

# shellcheck source=functions.sh
source /app/functions.sh

function print_version {
    if [[ -n "${COMPANION_VERSION:-}" ]]; then
        echo "Info: running acme-companion version ${COMPANION_VERSION}"
    fi
}

function check_docker_socket {
    if [[ $DOCKER_HOST == unix://* ]]; then
        socket_file=${DOCKER_HOST#unix://}
        if [[ ! -S $socket_file ]]; then
            if [[ ! -r $socket_file ]]; then
                echo "Warning: Docker host socket at $socket_file might not be readable. Please check user permissions" >&2
                echo "If you are in a SELinux environment, try using: '-v /var/run/docker.sock:$socket_file:z'" >&2
            fi
            echo "Error: you need to share your Docker host socket with a volume at $socket_file" >&2
            echo "Typically you should run your container with: '-v /var/run/docker.sock:$socket_file:ro'" >&2
            exit 1
        fi
    fi
}

function check_dir_is_mounted_volume {
    local dir="$1"
    if [[ $(get_self_cid) ]]; then
        if ! docker_api "/containers/$(get_self_cid)/json" | jq ".Mounts[].Destination" | grep -q "^\"$dir\"$"; then
            echo "Warning: '$dir' does not appear to be a mounted volume."
        fi
    else
        echo "Warning: can't check if '$dir' is a mounted volume without self container ID."
    fi
}

function check_writable_directory {
    local dir="$1"

    check_dir_is_mounted_volume "$dir"

    if [[ ! -d "$dir" ]]; then
        echo "Error: can't access to '$dir' directory !" >&2
        echo "Check that '$dir' directory is declared as a writable volume." >&2
        exit 1
    fi
    if ! touch "$dir/.check_writable" 2>/dev/null ; then
        echo "Error: can't write to the '$dir' directory !" >&2
        echo "Check that '$dir' directory is export as a writable volume." >&2
        exit 1
    fi
    rm -f "$dir/.check_writable"
}

function warn_html_directory {
    local dir='/usr/share/nginx/html'
    
    check_dir_is_mounted_volume "$dir"

    if [[ ! -d "$dir" ]] || ! touch "$dir/.check_writable" 2>/dev/null; then
        echo "Warning: can't access or write to '$dir' directory. This will prevent HTML-01 challenges from working correctly."
        echo "If you are only using DNS-01 challenges, you can ignore this warning, otherwise check that '$dir' is declared as a writable volume."
    fi
    rm -f "$dir/.check_writable"
}

function check_dh_group {
	# DH params will be supplied for acme-companion here:
	local DHPARAM_FILE='/etc/nginx/certs/dhparam.pem'

	# Should be 2048, 3072, or 4096 (default):
	local DHPARAM_BITS="${DHPARAM_BITS:=4096}"

    # Skip generation if DHPARAM_SKIP is set to true
    if parse_true "${DHPARAM_SKIP:=false}"; then
		echo "Info: Skipping Diffie-Hellman group setup."
		return 0
    fi

    # Let's check DHPARAM_BITS is set to a supported value
    if [[ ! "$DHPARAM_BITS" =~ ^(2048|3072|4096)$ ]]; then
        echo "Error: Unsupported DHPARAM_BITS size: ${DHPARAM_BITS}. Supported values are 2048, 3072, or 4096 (default)." >&2
        exit 1
    fi

    # Use an existing pre-generated DH group from RFC7919 (https://datatracker.ietf.org/doc/html/rfc7919#appendix-A):
    local RFC7919_DHPARAM_FILE="/app/dhparam/ffdhe${DHPARAM_BITS}.pem"
    local EXPECTED_DHPARAM_HASH; EXPECTED_DHPARAM_HASH=$(sha256sum "$RFC7919_DHPARAM_FILE" | cut -d ' ' -f1)

	# DH params may be provided by the user (rarely necessary)
	if [[ -f "$DHPARAM_FILE" ]]; then
        local USER_PROVIDED_DH

        # Check if the DH params file is user provided or comes from acme-companion
        local DHPARAM_HASH; DHPARAM_HASH=$(sha256sum "$DHPARAM_FILE" | cut -d ' ' -f1)
        
        for f in /app/dhparam/ffdhe*.pem; do
            local FFDHE_HASH; FFDHE_HASH=$(sha256sum "$f" | cut -d ' ' -f1)
            if [[ "$DHPARAM_HASH" == "$FFDHE_HASH" ]]; then
                # This is an acme-companion created DH params file
                USER_PROVIDED_DH='false'

                # Check if /etc/nginx/certs/dhparam.pem matches the expected pre-generated DH group
                if [[ "$DHPARAM_HASH" == "$EXPECTED_DHPARAM_HASH" ]]; then
                    set_ownership_and_permissions "$DHPARAM_FILE"
                    echo "Info: ${DHPARAM_BITS} bits RFC7919 Diffie-Hellman group found, generation skipped."
                    return 0
                fi
            fi
        done

        if parse_true "${USER_PROVIDED_DH:=true}"; then
            # This is a user provided DH params file
            set_ownership_and_permissions "$DHPARAM_FILE"
            echo "Info: A custom dhparam.pem file was provided. Best practice is to use standardized RFC7919 Diffie-Hellman groups instead."
            return 0
        fi
	fi

    # The RFC7919 DH params file either need to be created or replaced
	echo "Info: Setting up ${DHPARAM_BITS} bits RFC7919 Diffie-Hellman group..."
	cp "$RFC7919_DHPARAM_FILE" "${DHPARAM_FILE}.tmp"
    mv "${DHPARAM_FILE}.tmp" "$DHPARAM_FILE"
    set_ownership_and_permissions "$DHPARAM_FILE"
}

function check_default_cert_key {
    local cn='acme-companion'

    echo "Warning: there is no future support planned for the self signed default certificate creation feature and it might be removed in a future release."

    if [[ -e /etc/nginx/certs/default.crt && -e /etc/nginx/certs/default.key ]]; then
        default_cert_cn="$(openssl x509 -noout -subject -in /etc/nginx/certs/default.crt)"
        # Check if the existing default certificate is still valid for more
        # than 3 months / 7776000 seconds (60 x 60 x 24 x 30 x 3).
        check_cert_min_validity /etc/nginx/certs/default.crt 7776000
        cert_validity=$?
        [[ "$DEBUG" == 1 ]] && echo "Debug: a default certificate with $default_cert_cn is present."
    fi

    # Create a default cert and private key if:
    #   - either default.crt or default.key are absent
    #   OR
    #   - the existing default cert/key were generated by the container
    #     and the cert validity is less than three months
    if [[ ! -e /etc/nginx/certs/default.crt || ! -e /etc/nginx/certs/default.key ]] || [[ "${default_cert_cn:-}" =~ $cn && "${cert_validity:-}" -ne 0 ]]; then
        openssl req -x509 \
            -newkey rsa:4096 -sha256 -nodes -days 365 \
            -subj "/CN=$cn" \
            -keyout /etc/nginx/certs/default.key.new \
            -out /etc/nginx/certs/default.crt.new \
        && mv /etc/nginx/certs/default.key.new /etc/nginx/certs/default.key \
        && mv /etc/nginx/certs/default.crt.new /etc/nginx/certs/default.crt \
        && reload_nginx
        echo "Info: a default key and certificate have been created at /etc/nginx/certs/default.key and /etc/nginx/certs/default.crt."
    elif [[ "$DEBUG" == 1 && "${default_cert_cn:-}" =~ $cn ]]; then
        echo "Debug: the self generated default certificate is still valid for more than three months. Skipping default certificate creation."
    elif [[ "$DEBUG" == 1 ]]; then
        echo "Debug: the default certificate is user provided. Skipping default certificate creation."
    fi
    set_ownership_and_permissions "/etc/nginx/certs/default.key"
    set_ownership_and_permissions "/etc/nginx/certs/default.crt"
}

function check_default_account {
    # The default account is now for empty account email
    if [[ -f /etc/acme.sh/default/account.conf ]]; then
        if grep -q ACCOUNT_EMAIL /etc/acme.sh/default/account.conf; then
            sed -i '/ACCOUNT_EMAIL/d' /etc/acme.sh/default/account.conf
        fi
    fi
}

if [[ "$*" == "/bin/bash /app/start.sh" ]]; then
    print_version
    check_docker_socket
    if [[ -z "$(get_nginx_proxy_container)" ]]; then
        echo "Error: can't get nginx-proxy container ID !" >&2
        echo "Check that you are doing one of the following :" >&2
        echo -e "\t- Use the --volumes-from option to mount volumes from the nginx-proxy container." >&2
        echo -e "\t- Set the NGINX_PROXY_CONTAINER env var on the letsencrypt-companion container to the name of the nginx-proxy container." >&2
        echo -e "\t- Label the nginx-proxy container to use with 'com.github.nginx-proxy.nginx'." >&2
        exit 1
    elif [[ -z "$(get_docker_gen_container)" ]] && ! is_docker_gen_container "$(get_nginx_proxy_container)"; then
        echo "Error: can't get docker-gen container id !" >&2
        echo "If you are running a three containers setup, check that you are doing one of the following :" >&2
        echo -e "\t- Set the NGINX_DOCKER_GEN_CONTAINER env var on the letsencrypt-companion container to the name of the docker-gen container." >&2
        echo -e "\t- Label the docker-gen container to use with 'com.github.nginx-proxy.docker-gen'." >&2
        exit 1
    fi
    check_writable_directory '/etc/nginx/certs'
    parse_true "${ACME_HTTP_CHALLENGE_LOCATION:=false}" && check_writable_directory '/etc/nginx/vhost.d'
    check_writable_directory '/etc/acme.sh'
    warn_html_directory
    if [[ -f /app/letsencrypt_user_data ]]; then
        check_writable_directory '/etc/nginx/vhost.d'
        check_writable_directory '/etc/nginx/conf.d'
    fi
    parse_true "${CREATE_DEFAULT_CERTIFICATE:=false}" && check_default_cert_key
    check_dh_group
    reload_nginx
    check_default_account
fi

exec "$@"


================================================
FILE: app/force_renew
================================================
#!/bin/bash

# shellcheck source=letsencrypt_service
source /app/letsencrypt_service --source-only

update_certs --force-renew


================================================
FILE: app/functions.sh
================================================
#!/bin/bash

# Convert argument to lowercase (bash 4 only)
function lc {
	echo "${@,,}"
}

DEBUG="$(lc "${DEBUG:-}")"
if [[ "$DEBUG" == true ]]; then
  DEBUG=1 && export DEBUG
fi

function parse_true() {
	case "$1" in

		true | True | TRUE | 1)
		return 0
		;;

		*)
		return 1
		;;

	esac
}

function in_array() {
    local needle="$1" item
    local -n arrref="$2"
    for item in "${arrref[@]}"; do
        [[ "$item" == "$needle" ]] && return 0
    done
    return 1
}

[[ -z "${VHOST_DIR:-}" ]] && \
 declare -r VHOST_DIR=/etc/nginx/vhost.d
[[ -z "${START_HEADER:-}" ]] && \
 declare -r START_HEADER='## Start of configuration add by letsencrypt container'
[[ -z "${END_HEADER:-}" ]] && \
 declare -r END_HEADER='## End of configuration add by letsencrypt container'

function check_nginx_proxy_container_run {
    local _nginx_proxy_container; _nginx_proxy_container=$(get_nginx_proxy_container)
    if [[ -n "$_nginx_proxy_container" ]]; then
        if [[ $(docker_api "/containers/${_nginx_proxy_container}/json" | jq -r '.State.Status') = "running" ]];then
            return 0
        else
            echo "$(date "+%Y/%m/%d %T") Error: nginx-proxy container ${_nginx_proxy_container} isn't running." >&2
            return 1
        fi
    else
        echo "$(date "+%Y/%m/%d %T") Error: could not get a nginx-proxy container ID." >&2
        return 1
fi
}

function ascending_wildcard_locations {
    # Given foo.bar.baz.example.com as argument, will output:
    # - *.bar.baz.example.com
    # - *.baz.example.com
    # - *.example.com
    local domain="${1:?}"
    local first_label
    tld_regex="^[[:alpha:]]+$"
    regex="^[^.]+\..+$"
    while [[ "$domain" =~ $regex ]]; do
      first_label="${domain%%.*}"
      domain="${domain/#"${first_label}."/}"
      if [[ "$domain" == "*" || "$domain" =~ $tld_regex ]]; then
        return
      else
        echo "*.${domain}"
      fi
    done
}

function descending_wildcard_locations {
    # Given foo.bar.baz.example.com as argument, will output:
    # - foo.bar.baz.example.*
    # - foo.bar.baz.*
    # - foo.bar.*
    # - foo.*
    local domain="${1:?}"
    local last_label
    regex="^.+\.[^.]+$"
    while [[ "$domain" =~ $regex ]]; do
      last_label="${domain##*.}"
      domain="${domain/%".${last_label}"/}"
      if [[ "$domain" == "*" ]]; then
        return
      else
        echo "${domain}.*"
      fi
    done
}

function enumerate_wildcard_locations {
    # Goes through ascending then descending wildcard locations for a given FQDN
    local domain="${1:?}"
    ascending_wildcard_locations "$domain"
    descending_wildcard_locations "$domain"
}

function add_location_configuration {
    local domain="${1:-}"
    local wildcard_domain
    # If no domain was passed use default instead
    [[ -z "$domain" ]] && domain='default'

    # If the domain does not have an exact matching location file, test the possible
    # wildcard locations files. Use default is no location file is present at all.
    if [[ ! -f "${VHOST_DIR}/${domain}" ]]; then
      while read -r wildcard_domain; do
        if [[ -f "${VHOST_DIR}/${wildcard_domain}" ]]; then
          domain="$wildcard_domain"
          break
        fi
        domain='default'
      done <<< "$(enumerate_wildcard_locations "$domain")"
    fi

    if [[ -f "${VHOST_DIR}/${domain}" && -n $(sed -n "/$START_HEADER/,/$END_HEADER/p" "${VHOST_DIR}/${domain}") ]]; then
        # If the config file exist and already have the location configuration, end with exit code 0
        return 0
    else
        # Else write the location configuration to a temp file ...
        echo "$START_HEADER" > "${VHOST_DIR}/${domain}".new
        cat /app/nginx_location.conf >> "${VHOST_DIR}/${domain}".new
        echo "$END_HEADER" >> "${VHOST_DIR}/${domain}".new
        # ... append the existing file content to the temp one ...
        [[ -f "${VHOST_DIR}/${domain}" ]] && cat "${VHOST_DIR}/${domain}" >> "${VHOST_DIR}/${domain}".new
        # ... and copy the temp file to the old one (if the destination file is bind mounted, you can't change
        # its inode from within the container, so mv won't work and cp has to be used), then remove the temp file.
        cp -f "${VHOST_DIR}/${domain}".new "${VHOST_DIR}/${domain}" && rm -f "${VHOST_DIR}/${domain}".new
        return 1
    fi
}

function add_standalone_configuration {
    local domain="${1:?}"
    if grep -q "server_name ${domain};" /etc/nginx/conf.d/*.conf; then
        # If the domain is already present in nginx's conf, use the location configuration.
        add_location_configuration "$domain"
    else
        # Else use the standalone configuration.
        cat > "/etc/nginx/conf.d/standalone-cert-$domain.conf" << EOF
server {
    server_name $domain;
    listen 80;
    access_log /var/log/nginx/access.log vhost;
    location ^~ /.well-known/acme-challenge/ {
        auth_basic off;
        auth_request off;
        allow all;
        root /usr/share/nginx/html;
        try_files \$uri =404;
        break;
    }
}
EOF
    fi
}

function remove_all_standalone_configurations {
    local old_shopt_options; old_shopt_options=$(shopt -p) # Backup shopt options
    shopt -s nullglob
    for file in "/etc/nginx/conf.d/standalone-cert-"*".conf"; do
      rm -f "$file"
    done
    eval "$old_shopt_options" # Restore shopt options
}

function remove_all_location_configurations {
    for file in "${VHOST_DIR}"/*; do
        [[ -e "$file" ]] || continue
        if [[ -n $(sed -n "/$START_HEADER/,/$END_HEADER/p" "$file") ]]; then
            sed "/$START_HEADER/,/$END_HEADER/d" "$file" > "$file".new
            cp -f "$file".new "$file" && rm -f "$file".new
        fi
    done
}

function check_cert_min_validity {
    # Check if a certificate ($1) is still valid for a given amount of time in seconds ($2).
    # Returns 0 if the certificate is still valid for this amount of time, 1 otherwise.
    local cert_path="$1"
    local min_validity="$(( $(date "+%s") + $2 ))"

    local cert_expiration
    cert_expiration="$(openssl x509 -noout -enddate -in "$cert_path" | cut -d "=" -f 2)"
    cert_expiration="$(date --utc --date "${cert_expiration% GMT}" "+%s")"

    [[ $cert_expiration -gt $min_validity ]] || return 1
}

function get_self_cid {
    local self_cid=""

    # Try the /proc files methods first then resort to the Docker API.
    if [[ -f /proc/1/cpuset ]]; then
        self_cid="$(grep -Eo -m 1 '[[:alnum:]]{64}' /proc/1/cpuset)"
    fi
    if [[ ( ${#self_cid} != 64 ) && ( -f /proc/self/cgroup ) ]]; then
        self_cid="$(grep -Eo -m 1 '[[:alnum:]]{64}' /proc/self/cgroup)"
    fi
    # cgroups v2
    if [[ ( ${#self_cid} != 64 ) && ( -f /proc/self/mountinfo ) ]]; then
        self_cid="$(grep '/userdata/hostname' /proc/self/mountinfo | grep -Eo -m 1 '[[:alnum:]]{64}')"
    fi
    if [[ ( ${#self_cid} != 64 ) ]]; then
        self_cid="$(docker_api "/containers/$(hostname)/json" | jq -r '.Id')"
    fi

    # If it's not 64 characters long, then it's probably not a container ID.
    if [[ ${#self_cid} == 64 ]]; then
        echo "$self_cid"
    else
        echo "$(date "+%Y/%m/%d %T"), Error: can't get my container ID !" >&2
        return 1
    fi
}

## Docker API
function docker_api {
    local scheme
    local curl_opts=(-s)
    local method=${2:-GET}
    # data to POST
    if [[ -n "${3:-}" ]]; then
        curl_opts+=(-d "$3")
    fi
    if [[ -z "$DOCKER_HOST" ]];then
        echo "Error DOCKER_HOST variable not set" >&2
        return 1
    fi
    if [[ $DOCKER_HOST == unix://* ]]; then
        curl_opts+=(--unix-socket "${DOCKER_HOST#unix://}")
        scheme='http://localhost'
    else
        scheme="http://${DOCKER_HOST#*://}"
    fi
    [[ $method = "POST" ]] && curl_opts+=(-H 'Content-Type: application/json')
    curl "${curl_opts[@]}" -X "${method}" "${scheme}$1"
}

function docker_exec {
    local id="${1?missing id}"
    local cmd="${2?missing command}"
    local data; data=$(printf '{ "AttachStdin": false, "AttachStdout": true, "AttachStderr": true, "Tty":false,"Cmd": %s }' "$cmd")
    exec_id=$(docker_api "/containers/$id/exec" "POST" "$data" | jq -r .Id)
    if [[ -n "$exec_id" && "$exec_id" != "null" ]]; then
        docker_api "/exec/${exec_id}/start" "POST" '{"Detach": false, "Tty":false}'
    else
        echo "$(date "+%Y/%m/%d %T"), Error: can't exec command ${cmd} in container ${id}. Check if the container is running." >&2
        return 1
    fi
}

function docker_restart {
    local id="${1?missing id}"
    docker_api "/containers/$id/restart" "POST"
}

function docker_kill {
    local id="${1?missing id}"
    local signal="${2?missing signal}"
    docker_api "/containers/$id/kill?signal=$signal" "POST"
}

function labeled_cid {
    docker_api "/containers/json" | jq -r '.[] | select(.Labels["'"$1"'"])|.Id'
}

function is_docker_gen_container {
    local id="${1?missing id}"
    if [[ $(docker_api "/containers/$id/json" | jq -r '.Config.Env[]' | grep -c -E '^DOCKER_GEN_VERSION=') = "1" ]]; then
        return 0
    else
        return 1
    fi
}

function get_docker_gen_container {
    # First try to get the docker-gen container ID from the container label.
    local legacy_docker_gen_cid; legacy_docker_gen_cid="$(labeled_cid com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen)"
    local new_docker_gen_cid; new_docker_gen_cid="$(labeled_cid com.github.nginx-proxy.docker-gen)"
    local docker_gen_cid; docker_gen_cid="${new_docker_gen_cid:-$legacy_docker_gen_cid}"

    # If the labeled_cid function dit not return anything and the env var is set, use it.
    if [[ -z "$docker_gen_cid" ]] && [[ -n "${NGINX_DOCKER_GEN_CONTAINER:-}" ]]; then
        docker_gen_cid="$NGINX_DOCKER_GEN_CONTAINER"
    fi

    # If a container ID was found, output it. The function will return 1 otherwise.
    [[ -n "$docker_gen_cid" ]] && echo "$docker_gen_cid"
}

function get_nginx_proxy_container {
    local volumes_from
    # First try to get the nginx container ID from the container label.
    local legacy_nginx_cid; legacy_nginx_cid="$(labeled_cid com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy)"
    local new_nginx_cid; new_nginx_cid="$(labeled_cid com.github.nginx-proxy.nginx)"
    local nginx_cid; nginx_cid="${new_nginx_cid:-$legacy_nginx_cid}"

    # If the labeled_cid function dit not return anything ...
    if [[ -z "${nginx_cid}" ]]; then
        # ... and the env var is set, use it ...
        if [[ -n "${NGINX_PROXY_CONTAINER:-}" ]]; then
            nginx_cid="$NGINX_PROXY_CONTAINER"
        # ... else try to get the container ID with the volumes_from method.
        elif [[ $(get_self_cid) ]]; then
            volumes_from=$(docker_api "/containers/$(get_self_cid)/json" | jq -r '.HostConfig.VolumesFrom[]' 2>/dev/null)
            for cid in $volumes_from; do
                cid="${cid%:*}" # Remove leading :ro or :rw set by remote docker-compose (thx anoopr)
                if [[ $(docker_api "/containers/$cid/json" | jq -r '.Config.Env[]' | grep -c -E '^NGINX_VERSION=') = "1" ]];then
                    nginx_cid="$cid"
                    break
                fi
            done
        fi
    fi

    # If a container ID was found, output it. The function will return 1 otherwise.
    [[ -n "$nginx_cid" ]] && echo "$nginx_cid"
}

## Nginx
function reload_nginx {
    local _docker_gen_container; _docker_gen_container=$(get_docker_gen_container)
    local _nginx_proxy_container; _nginx_proxy_container=$(get_nginx_proxy_container)

    if [[ -n "${_docker_gen_container:-}" ]]; then
        # Using docker-gen and nginx in separate container
        echo "Reloading nginx docker-gen (using separate container ${_docker_gen_container})..."
        docker_kill "${_docker_gen_container}" SIGHUP

        if [[ -n "${_nginx_proxy_container:-}" ]]; then
            # Reloading nginx in case only certificates had been renewed
            echo "Reloading nginx (using separate container ${_nginx_proxy_container})..."
            docker_kill "${_nginx_proxy_container}" SIGHUP
        fi
    else
        if [[ -n "${_nginx_proxy_container:-}" ]]; then
            echo "Reloading nginx proxy (${_nginx_proxy_container})..."
            docker_exec "${_nginx_proxy_container}" \
                '[ "sh", "-c", "/app/docker-entrypoint.sh /usr/local/bin/docker-gen /app/nginx.tmpl /etc/nginx/conf.d/default.conf; /usr/sbin/nginx -s reload" ]' \
                | sed -rn 's/^.*([0-9]{4}\/[0-9]{2}\/[0-9]{2}.*$)/\1/p'
            [[ ${PIPESTATUS[0]} -eq 1 ]] && echo "$(date "+%Y/%m/%d %T"), Error: can't reload nginx-proxy." >&2
        fi
    fi
}

function set_ownership_and_permissions {
  local path="${1:?}"
  # The default ownership is root:root, with 755 permissions for folders and 600 for private files.
  local user="${FILES_UID:-root}"
  local group="${FILES_GID:-$user}"
  local f_perms="${FILES_PERMS:-600}"
  local d_perms="${FOLDERS_PERMS:-755}"

  if [[ ! "$f_perms" =~ ^[0-7]{3,4}$ ]]; then
    echo "Warning : the provided files permission octal ($f_perms) is incorrect. Skipping ownership and permissions check."
    return 1
  fi
  if [[ ! "$d_perms" =~ ^[0-7]{3,4}$ ]]; then
    echo "Warning : the provided folders permission octal ($d_perms) is incorrect. Skipping ownership and permissions check."
    return 1
  fi

  [[ "$DEBUG" == 1 ]] && echo "Debug: checking $path ownership and permissions."

  # Find the user numeric ID if the FILES_UID environment variable isn't numeric.
  if [[ "$user" =~ ^[0-9]+$ ]]; then
    user_num="$user"
  # Check if this user exist inside the container
  elif id -u "$user" > /dev/null 2>&1; then
    # Convert the user name to numeric ID
    local user_num; user_num="$(id -u "$user")"
    [[ "$DEBUG" == 1 ]] && echo "Debug: numeric ID of user $user is $user_num."
  else
    echo "Warning: user $user not found in the container, please use a numeric user ID instead of a user name. Skipping ownership and permissions check."
    return 1
  fi

  # Find the group numeric ID if the FILES_GID environment variable isn't numeric.
  if [[ "$group" =~ ^[0-9]+$ ]]; then
    group_num="$group"
  # Check if this group exist inside the container
  elif getent group "$group" > /dev/null 2>&1; then
    # Convert the group name to numeric ID
    local group_num; group_num="$(getent group "$group" | awk -F ':' '{print $3}')"
    [[ "$DEBUG" == 1 ]] && echo "Debug: numeric ID of group $group is $group_num."
  else
    echo "Warning: group $group not found in the container, please use a numeric group ID instead of a group name. Skipping ownership and permissions check."
    return 1
  fi

  # Check and modify ownership if required.
  if [[ -e "$path" ]]; then
    if [[ "$(stat -c %u:%g "$path" )" != "$user_num:$group_num" ]]; then
      [[ "$DEBUG" == 1 ]] && echo "Debug: setting $path ownership to $user:$group."
      if [[ -L "$path" ]]; then
        chown -h "$user_num:$group_num" "$path"
      else
        chown "$user_num:$group_num" "$path"
      fi
    fi
    # If the path is a folder, check and modify permissions if required.
    if [[ -d "$path" ]]; then
      if [[ "$(stat -c %a "$path")" != "$d_perms" ]]; then
        [[ "$DEBUG" == 1 ]] && echo "Debug: setting $path permissions to $d_perms."
        chmod "$d_perms" "$path"
      fi
    # If the path is a file, check and modify permissions if required.
    elif [[ -f "$path" ]]; then
      # Use different permissions for private files (private keys and ACME account files) ...
      if [[ "$path" =~ ^.*(key\.pem|\.key)$ ]]; then
        if [[ "$(stat -c %a "$path")" != "$f_perms" ]]; then
          [[ "$DEBUG" == 1 ]] && echo "Debug: setting $path permissions to $f_perms."
          chmod "$f_perms" "$path"
        fi
      # ... and for public files (certificates, chains, fullchains, DH parameters).
      else
        if [[ "$(stat -c %a "$path")" != "644" ]]; then
          [[ "$DEBUG" == 1 ]] && echo "Debug: setting $path permissions to 644."
          chmod "644" "$path"
        fi
      fi
    fi
  else
    echo "Warning: $path does not exist. Skipping ownership and permissions check."
    return 1
  fi
}


================================================
FILE: app/letsencrypt_service
================================================
#!/bin/bash

# shellcheck source=functions.sh
source /app/functions.sh

CERTS_UPDATE_INTERVAL="${CERTS_UPDATE_INTERVAL:-3600}"
ACME_CA_URI="${ACME_CA_URI:-"https://acme-v02.api.letsencrypt.org/directory"}"
ACME_CA_TEST_URI="https://acme-staging-v02.api.letsencrypt.org/directory"
DEFAULT_KEY_SIZE="${DEFAULT_KEY_SIZE:-4096}"
RENEW_PRIVATE_KEYS="$(lc "${RENEW_PRIVATE_KEYS:-true}")"
DEFAULT_RENEW="${DEFAULT_RENEW:-60}"

# Backward compatibility environment variable
REUSE_PRIVATE_KEYS="$(lc "${REUSE_PRIVATE_KEYS:-false}")"

function strip_wildcard {
    # Remove wildcard prefix if present
    # https://github.com/nginx-proxy/nginx-proxy/tree/main/docs#wildcard-certificates
    local -r domain="${1?missing domain argument}"
    if [[ "${domain:0:2}" == "*." ]]; then
        echo "${domain:2}"
    else
        echo "$domain"
    fi
}

function create_link {
    local -r source=${1?missing source argument}
    local -r target=${2?missing target argument}
    if [[ -f "$target" ]] && [[ "$(readlink "$target")" == "$source" ]]; then
      set_ownership_and_permissions "$target"
      [[ "$DEBUG" == 1 ]] && echo "$target already linked to $source"
      return 1
    else
      ln -sf "$source" "$target" \
        && set_ownership_and_permissions "$target"
    fi
}

function create_links {
    local -r base_domain=${1?missing base_domain argument}
    local domain=${2?missing base_domain argument}
    domain="$(strip_wildcard "$domain")"

    if [[ ! -f "/etc/nginx/certs/$base_domain/fullchain.pem" || \
          ! -f "/etc/nginx/certs/$base_domain/key.pem" ]]; then
        return 1
    fi
    local return_code=1
    create_link "./$base_domain/fullchain.pem" "/etc/nginx/certs/$domain.crt"
    return_code=$(( return_code & $? ))
    create_link "./$base_domain/key.pem" "/etc/nginx/certs/$domain.key"
    return_code=$(( return_code & $? ))
    if [[ -f "/etc/nginx/certs/dhparam.pem" ]]; then
        create_link ./dhparam.pem "/etc/nginx/certs/$domain.dhparam.pem"
        return_code=$(( return_code & $? ))
    fi
    if [[ -f "/etc/nginx/certs/$base_domain/chain.pem" ]]; then
        create_link "./$base_domain/chain.pem" "/etc/nginx/certs/$domain.chain.pem"
        return_code=$(( return_code & $? ))
    fi
    return $return_code
}

function cleanup_links {
    local -a LETSENCRYPT_CONTAINERS
    local -a LETSENCRYPT_STANDALONE_CERTS
    local -a ENABLED_DOMAINS
    local -a SYMLINKED_DOMAINS
    local -a DISABLED_DOMAINS

    # Create an array containing domains for which a symlinked certificate
    # exists in /etc/nginx/certs (excluding default cert).
    for symlinked_domain in /etc/nginx/certs/*.crt; do
        [[ -L "$symlinked_domain" ]] || continue
        symlinked_domain="${symlinked_domain##*/}"
        symlinked_domain="${symlinked_domain%*.crt}"
        [[ "$symlinked_domain" != "default" ]] || continue
        SYMLINKED_DOMAINS+=("$symlinked_domain")
    done
    [[ "$DEBUG" == 1 ]] && echo "Symlinked domains: ${SYMLINKED_DOMAINS[*]}"

    # Create an array containing domains that are considered
    # enabled (ie present on /app/letsencrypt_service_data or /app/letsencrypt_user_data).
    [[ -f /app/letsencrypt_service_data ]] && source /app/letsencrypt_service_data
    [[ -f /app/letsencrypt_user_data ]] && source /app/letsencrypt_user_data
    LETSENCRYPT_CONTAINERS+=( "${LETSENCRYPT_STANDALONE_CERTS[@]}" )
    for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
      local -n hosts_array="LETSENCRYPT_${cid}_HOST"
      for domain in "${hosts_array[@]}"; do
        domain="$(strip_wildcard "$domain")"
        # Add domain to the array storing currently enabled domains.
        ENABLED_DOMAINS+=("$domain")
      done
    done
    [[ "$DEBUG" == 1 ]] && echo "Enabled domains: ${ENABLED_DOMAINS[*]}"

    # Create an array containing only domains for which a symlinked private key exists
    # in /etc/nginx/certs but that no longer have a corresponding LETSENCRYPT_HOST set
    # on an active container or on /app/letsencrypt_user_data
    if [[ ${#SYMLINKED_DOMAINS[@]} -gt 0 ]]; then
        mapfile -t DISABLED_DOMAINS < <(echo "${SYMLINKED_DOMAINS[@]}" \
                                             "${ENABLED_DOMAINS[@]}" \
                                             "${ENABLED_DOMAINS[@]}" \
                                             | tr ' ' '\n' | sort | uniq -u)
    fi
    [[ "$DEBUG" == 1 ]] && echo "Disabled domains: ${DISABLED_DOMAINS[*]}"


    # Remove disabled domains symlinks if present.
    # Return 1 if nothing was removed and 0 otherwise.
    if [[ ${#DISABLED_DOMAINS[@]} -gt 0 ]]; then
      [[ "$DEBUG" == 1 ]] && echo "Some domains are disabled :"
      for disabled_domain in "${DISABLED_DOMAINS[@]}"; do
          [[ "$DEBUG" == 1 ]] && echo "Checking domain ${disabled_domain}"
          cert_folder="$(readlink -f "/etc/nginx/certs/${disabled_domain}.crt")"
          # If the dotfile is absent, skip domain.
          if [[ ! -e "${cert_folder%/*}/.companion" ]]; then
              [[ "$DEBUG" == 1 ]] && echo "No .companion file found in ${cert_folder}. ${disabled_domain} is not managed by acme-companion. Skipping domain."
              continue
          else
              [[ "$DEBUG" == 1 ]] && echo "${disabled_domain} is managed by acme-companion. Removing unused symlinks."
          fi

          for extension in .crt .key .dhparam.pem .chain.pem; do
              file="${disabled_domain}${extension}"
              if [[ -n "${file// }" ]] && [[ -L "/etc/nginx/certs/${file}" ]]; then
                  [[ "$DEBUG" == 1 ]] && echo "Removing /etc/nginx/certs/${file}"
                  rm -f "/etc/nginx/certs/${file}"
              fi
          done
      done
      return 0
    else
      return 1
    fi
}

function update_cert {
    local cid="${1:?}"
    local -n hosts_array="LETSENCRYPT_${cid}_HOST"
    # First domain will be our base domain
    local base_domain="${hosts_array[0]}"

    local wildcard_certificate='false'
    if [[ "${base_domain:0:2}" == "*." ]]; then
        wildcard_certificate='true'
    fi

    local should_restart_container='false'

    # Base CLI parameters array, used for both --register-account and --issue
    local -a params_base_arr
    params_base_arr+=(--log /dev/null)
    params_base_arr+=(--useragent "nginx-proxy/acme-companion/$COMPANION_VERSION (acme.sh/$ACMESH_VERSION)")
    [[ "$DEBUG" == 1 ]] && params_base_arr+=(--debug 2)

    # Alternative trusted root CA path, used for test with Pebble
    if [[ -n "${CA_BUNDLE// }" ]]; then
        if [[ -f "$CA_BUNDLE" ]]; then
            params_base_arr+=(--ca-bundle "$CA_BUNDLE")
            [[ "$DEBUG" == 1 ]] && echo "Debug: acme.sh will use $CA_BUNDLE as trusted root CA."
        else
            echo "Warning: the path to the alternate CA bundle ($CA_BUNDLE) is not valid, using default Alpine trust store."
        fi
    fi

    # CLI parameters array used for --register-account
    local -a params_register_arr

    # CLI parameters array used for --issue
    local -a params_issue_arr

    # ACME challenge type
    local -n acme_challenge="ACME_${cid}_CHALLENGE"
    if [[ -z "${acme_challenge}" ]]; then
        acme_challenge="${ACME_CHALLENGE:-HTTP-01}"
    fi
    
    if [[ "$acme_challenge" == "HTTP-01" ]]; then
        # HTTP-01 challenge
        if [[ "$wildcard_certificate" == 'true' ]]; then
            echo "Error: wildcard certificates (${base_domain}) can't be obtained with HTTP-01 challenge"
            return 1
        fi
        params_issue_arr+=(--webroot /usr/share/nginx/html)
    elif [[ "$acme_challenge" == "DNS-01" ]]; then
        # DNS-01 challenge
        local acmesh_dns_config_used='none'

        local default_acmesh_dns_api="${DEFAULT_ACMESH_DNS_API_CONFIG[DNS_API]}"
        [[ -n "$default_acmesh_dns_api" ]] && acmesh_dns_config_used='default'

        local -n acmesh_dns_config="ACMESH_${cid}_DNS_API_CONFIG"
        local acmesh_dns_api="${acmesh_dns_config[DNS_API]}"
        [[ -n "$acmesh_dns_api" ]] && acmesh_dns_config_used='container'

        local -a dns_api_keys

        case "$acmesh_dns_config_used" in
            'default')
                params_issue_arr+=(--dns "$default_acmesh_dns_api")
                # Loop over defined variable for default acme.sh DNS api config
                for key in "${!DEFAULT_ACMESH_DNS_API_CONFIG[@]}"; do
                    [[ "$key" == "DNS_API" ]] && continue
                    dns_api_keys+=("$key")
                    local value="${DEFAULT_ACMESH_DNS_API_CONFIG[$key]}"
                    local -x "$key"="$value"
                done
                ;;
            'container')
                params_issue_arr+=(--dns "$acmesh_dns_api")
                # Loop over defined variable for per container acme.sh DNS api config
                for key in "${!acmesh_dns_config[@]}"; do
                    [[ "$key" == "DNS_API" ]] && continue
                    dns_api_keys+=("$key")
                    local value="${acmesh_dns_config[$key]}"
                    local -x "$key"="$value"
                done
                ;;
            *)
                echo "Error: missing acme.sh DNS API for DNS challenge"
                return 1
                ;;
        esac

        echo "Info: DNS challenge using $acmesh_dns_api DNS API with the following keys: ${dns_api_keys[*]} (${acmesh_dns_config_used} config)"
    else 
        echo "Error: unknown ACME challenge method: $acme_challenge"
        return 1
    fi

    local -n cert_keysize="LETSENCRYPT_${cid}_KEYSIZE"
    if [[ -z "$cert_keysize" ]] || \
        [[ ! "$cert_keysize" =~ ^('2048'|'3072'|'4096'|'ec-256'|'ec-384')$ ]]; then
        cert_keysize=$DEFAULT_KEY_SIZE
    fi
    params_issue_arr+=(--keylength "$cert_keysize")

    # OCSP-Must-Staple extension
    local -n ocsp="ACME_${cid}_OCSP"
    if [[ $(lc "$ocsp") == true ]]; then
        params_issue_arr+=(--ocsp-must-staple)
    fi

    local -n accountemail="LETSENCRYPT_${cid}_EMAIL"
    local config_home
    # If we don't have a LETSENCRYPT_EMAIL from the proxied container
    # and DEFAULT_EMAIL is set to a non empty value, use the latter.
    if [[ -z "$accountemail" ]]; then
        if [[ -n "${DEFAULT_EMAIL// }" ]]; then
            accountemail="$DEFAULT_EMAIL"
        else
            unset accountemail
        fi
    fi
    if [[ -n "${accountemail// }" ]]; then
        # If we got an email, use it with the corresponding config home
        config_home="/etc/acme.sh/$accountemail"
    else
        # If we did not get any email at all, use the default (empty mail) config
        config_home="/etc/acme.sh/default"
    fi

    local -n acme_ca_uri="ACME_${cid}_CA_URI"
    if [[ -z "$acme_ca_uri" ]]; then
        # Use default or user provided ACME end point
        acme_ca_uri="$ACME_CA_URI"
    fi
    # LETSENCRYPT_TEST overrides LETSENCRYPT_ACME_CA_URI
    local -n test_certificate="LETSENCRYPT_${cid}_TEST"
    if [[ $(lc "$test_certificate") == true ]]; then
        # Use Let's Encrypt ACME V2 staging end point
        acme_ca_uri="$ACME_CA_TEST_URI"
    fi

    # Set relevant --server parameter and ca folder name
    params_base_arr+=(--server "$acme_ca_uri")

    # Reproduce acme.sh logic to determine the ca account folder path
    local ca_host_dir
    ca_host_dir="$(echo "$acme_ca_uri" | cut -d : -f 2 | tr -s / | cut -d / -f 2)"
    local ca_path_dir
    ca_path_dir="$(echo "$acme_ca_uri" | cut -d : -f 2- | tr -s / | cut -d / -f 3-)"

    local relative_certificate_dir
    if [[ "$wildcard_certificate" == 'true' ]]; then
        relative_certificate_dir="wildcard_${base_domain:2}"
    else
        relative_certificate_dir="$base_domain"
    fi
    # If we're going to use one of LE stating endpoints ...
    if [[ "$acme_ca_uri" =~ ^https://acme-staging.* ]]; then
        # Unset accountemail
        # force config dir to 'staging'
        unset accountemail
        config_home="/etc/acme.sh/staging"
        # Prefix test certificate directory with _test_
        relative_certificate_dir="_test_${relative_certificate_dir}"
    fi

    local absolute_certificate_dir="/etc/nginx/certs/$relative_certificate_dir"
    params_issue_arr+=( \
        --cert-file "${absolute_certificate_dir}/cert.pem" \
        --key-file "${absolute_certificate_dir}/key.pem" \
        --ca-file "${absolute_certificate_dir}/chain.pem" \
        --fullchain-file "${absolute_certificate_dir}/fullchain.pem" \
    )

    [[ ! -d "$config_home" ]] && mkdir -p "$config_home"
    params_base_arr+=(--config-home "$config_home")
    local account_file="${config_home}/ca/${ca_host_dir}/${ca_path_dir}/account.json"

    # External Account Binding (EAB)
    local -n eab_kid="ACME_${cid}_EAB_KID"
    local -n eab_hmac_key="ACME_${cid}_EAB_HMAC_KEY"
    if [[ ! -f "$account_file" ]]; then
        if [[ -n "${eab_kid}" && -n "${eab_hmac_key}" ]]; then
            # Register the ACME account with the per container EAB credentials.
            params_register_arr+=(--eab-kid "$eab_kid" --eab-hmac-key "$eab_hmac_key")
        elif [[ -n "${ACME_EAB_KID// }" && -n "${ACME_EAB_HMAC_KEY// }" ]]; then
            # We don't have per-container EAB kid and hmac key or Zero SSL API key.
            # Register the ACME account with the default EAB credentials.
            params_register_arr+=(--eab-kid "$ACME_EAB_KID" --eab-hmac-key "$ACME_EAB_HMAC_KEY")
        elif [[ -n "${accountemail// }" ]]; then
            # We don't have per container nor default EAB credentials, register a new account.
            params_register_arr+=(--accountemail "$accountemail")
        fi
    fi

    # Zero SSL
    if [[ "$acme_ca_uri" == "https://acme.zerossl.com/v2/DV90" ]]; then
        # Test if we already have:
        #   - an account file
        #   - the --accountemail account registration parameter
        #   - the --eab-kid and --eab-hmac-key account registration parameters
        local account_ok='false'
        if [[ -f "$account_file" ]]; then
            account_ok='true'
        elif in_array '--accountemail' 'params_register_arr'; then
            account_ok='true'
        elif in_array '--eab-kid' 'params_register_arr' && in_array '--eab-hmac-key' 'params_register_arr'; then
            account_ok='true'
        fi

        if [[ $account_ok == 'false' ]]; then
            local -n zerossl_api_key="ZEROSSL_${cid}_API_KEY"
            if [[ -z "$zerossl_api_key" ]]; then
                # Try using the default API key
                zerossl_api_key="${ZEROSSL_API_KEY:-}"
            fi

            if [[ -n "${zerossl_api_key// }" ]]; then
                # Generate a set of ACME EAB credentials using the ZeroSSL API.
                local zerossl_api_response
                if zerossl_api_response="$(curl -s -X POST "https://api.zerossl.com/acme/eab-credentials?access_key=${zerossl_api_key}")"; then
                    if [[ "$(jq -r .success <<< "$zerossl_api_response")" == 'true' ]]; then
                        eab_kid="$(jq -r .eab_kid <<< "$zerossl_api_response")"
                        eab_hmac_key="$(jq -r .eab_hmac_key <<< "$zerossl_api_response")"
                        params_register_arr+=(--eab-kid "$eab_kid" --eab-hmac-key "$eab_hmac_key")
                        [[ "$DEBUG" == 1 ]] && echo "Successfull EAB credentials request against the ZeroSSL API, got the following EAB kid : ${eab_kid}"
                    else
                        # The JSON response body indicated an unsuccesfull API call.
                        echo "Warning: the EAB credentials request against the ZeroSSL API was not successfull."
                    fi
                else
                    # curl failed.
                    echo "Warning: curl failed to make an HTTP POST request to https://api.zerossl.com/acme/eab-credentials."
                fi
            else
                # We don't have a Zero SSL ACME account, EAB credentials, a ZeroSSL API key or an account email :
                # skip certificate account registration and certificate issuance.
                echo "Error: usage of ZeroSSL require an email bound account. No EAB credentials, ZeroSSL API key or email were provided for this certificate, creation aborted."
                return 1
            fi
        fi
    fi

    # Account registration and update if required
    if [[ ! -f "$account_file" ]]; then
        params_register_arr=("${params_base_arr[@]}" "${params_register_arr[@]}")
        [[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --register-account with the following parameters : ${params_register_arr[*]}"
        acme.sh --register-account "${params_register_arr[@]}"
    fi
    if [[ -n "${accountemail// }" ]] && ! grep -q "mailto:$accountemail" "$account_file"; then
        local -a params_update_arr=("${params_base_arr[@]}" --accountemail "$accountemail")
        [[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --update-account with the following parameters : ${params_update_arr[*]}"
        acme.sh --update-account "${params_update_arr[@]}"
    fi

    # If we still don't have an account.json file by this point, we've got an issue
    if [[ ! -f "$account_file" ]]; then
        echo "Error: no ACME account was found or registered for $accountemail and $acme_ca_uri, certificate creation aborted."
        return 1
    fi

    # acme.sh pre and post hooks
    local -n acme_pre_hook="ACME_${cid}_PRE_HOOK"
    if [[ -n "${acme_pre_hook}" ]]; then
        # Use per-container pre hook
	    params_issue_arr+=(--pre-hook "$acme_pre_hook")
    elif [[ -n ${ACME_PRE_HOOK// } ]]; then
        # Use default pre hook
        params_issue_arr+=(--pre-hook "$ACME_PRE_HOOK")
    fi

    local -n acme_post_hook="ACME_${cid}_POST_HOOK"
    if [[ -n "${acme_post_hook}" ]]; then
        # Use per-container post hook
	    params_issue_arr+=(--post-hook "$acme_post_hook")
    elif [[ -n ${ACME_POST_HOOK// } ]]; then
        # Use default post hook
        params_issue_arr+=(--post-hook "$ACME_POST_HOOK")
    fi

    local -n acme_preferred_chain="ACME_${cid}_PREFERRED_CHAIN"
    if [[ -n "${acme_preferred_chain}" ]]; then
        # Using amce.sh --preferred-chain to select alternate chain.
        params_issue_arr+=(--preferred-chain "$acme_preferred_chain")
    fi
    if [[ "$RENEW_PRIVATE_KEYS" != 'false' && "$REUSE_PRIVATE_KEYS" != 'true' ]]; then
        params_issue_arr+=(--always-force-new-domain-key)
    fi
    [[ "${2:-}" == "--force-renew" ]] && params_issue_arr+=(--force)

    # Create directory for the first domain
    mkdir -p "$absolute_certificate_dir"
    set_ownership_and_permissions "$absolute_certificate_dir"

    for domain in "${hosts_array[@]}"; do
        # Add all the domains to certificate
        params_issue_arr+=(--domain "$domain")
        # If enabled, add location configuration for the domain
        if [[ "$acme_challenge" == "HTTP-01" ]] && parse_true "${ACME_HTTP_CHALLENGE_LOCATION:=false}"; then
            add_location_configuration "$domain" || reload_nginx
        fi
    done

    # Allow to override day to renew cert
    params_issue_arr+=(--days "$DEFAULT_RENEW")

    params_issue_arr=("${params_base_arr[@]}" "${params_issue_arr[@]}")
    [[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --issue with the following parameters : ${params_issue_arr[*]}"
    echo "Creating/renewal $base_domain certificates... (${hosts_array[*]})"
    acme.sh --issue "${params_issue_arr[@]}"

    local acmesh_return=$?

    # 0 = success, 2 = RENEW_SKIP
    if [[ $acmesh_return == 0 || $acmesh_return == 2 ]]; then
        for domain in "${hosts_array[@]}"; do
            create_links "$relative_certificate_dir" "$domain" \
                && should_reload_nginx='true' \
                && should_restart_container='true'
        done
        echo "${COMPANION_VERSION:-}" > "${absolute_certificate_dir}/.companion"
        set_ownership_and_permissions "${absolute_certificate_dir}/.companion"
        # Make private key root readable only
        for file in cert.pem key.pem chain.pem fullchain.pem; do
            local file_path="${absolute_certificate_dir}/${file}"
            [[ -e "$file_path" ]] && set_ownership_and_permissions "$file_path"
        done
        local acme_private_key
        acme_private_key="$(find /etc/acme.sh -name "*.key" -path "*${hosts_array[0]}*")"
        [[ -e "$acme_private_key" ]] && set_ownership_and_permissions "$acme_private_key"
        # Queue nginx reload if a certificate was issued or renewed
        [[ $acmesh_return -eq 0 ]] \
            && should_reload_nginx='true' \
            && should_restart_container='true'
    fi

    # Restart container if certs are updated and the respective environmental variable is set
    local -n restart_container="LETSENCRYPT_${cid}_RESTART_CONTAINER"
    if [[ $(lc "$restart_container") == true ]] && [[ "$should_restart_container" == 'true' ]]; then
        echo "Restarting container (${cid})..."
        docker_restart "${cid}"
    fi

    for domain in "${hosts_array[@]}"; do
        if [[ -f "/etc/nginx/conf.d/standalone-cert-$domain.conf" ]]; then
            [[ "$DEBUG" == 1 ]] && echo "Debug: removing standalone configuration file /etc/nginx/conf.d/standalone-cert-$domain.conf"
            rm -f "/etc/nginx/conf.d/standalone-cert-$domain.conf" && should_reload_nginx='true'
        fi
    done

    if ! parse_true "${RELOAD_NGINX_ONLY_ONCE:-false}" && parse_true $should_reload_nginx; then
        reload_nginx
    fi
}

function update_certs {
    local -a LETSENCRYPT_CONTAINERS
    local -a LETSENCRYPT_STANDALONE_CERTS

    pushd /etc/nginx/certs > /dev/null || return
    check_nginx_proxy_container_run || return

    # Load relevant container settings
    if [[ -f /app/letsencrypt_service_data ]]; then
        source /app/letsencrypt_service_data
    else
        echo "Warning: /app/letsencrypt_service_data not found, skipping data from containers."
    fi

    # Load settings for standalone certs
    if [[ -f /app/letsencrypt_user_data ]]; then
        if source /app/letsencrypt_user_data; then
            for cid in "${LETSENCRYPT_STANDALONE_CERTS[@]}"; do
                local -n hosts_array="LETSENCRYPT_${cid}_HOST"
                
                local -n acme_challenge="ACME_${cid}_CHALLENGE"
                acme_challenge="${acme_challenge:-HTTP-01}"
                
                if [[ "$acme_challenge" == "HTTP-01" ]]; then
                    for domain in "${hosts_array[@]}"; do
                        add_standalone_configuration "$domain"
                    done
                fi
            done
            reload_nginx
            LETSENCRYPT_CONTAINERS+=( "${LETSENCRYPT_STANDALONE_CERTS[@]}" )
        else
            echo "Warning: could not source /app/letsencrypt_user_data, skipping user data"
        fi
    fi

    should_reload_nginx='false'
    for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
        # Pass the eventual --force-renew arg to update_cert() as second arg
        update_cert "$cid" "${1:-}"
    done

    cleanup_links && should_reload_nginx='true'

    [[ "$should_reload_nginx" == 'true' ]] && reload_nginx

    popd > /dev/null || return
}

# Allow the script functions to be sourced without starting the Service Loop.
if [ "${1}" == "--source-only" ]; then
  return 0
fi

pid=
# Service Loop: When this script exits, start it again.
trap '[[ $pid ]] && kill $pid; exec $0' EXIT
trap 'trap - EXIT' INT TERM

update_certs "$@"

# Wait some amount of time
echo "Sleep for ${CERTS_UPDATE_INTERVAL}s"
sleep $CERTS_UPDATE_INTERVAL & pid=$!
wait
pid=


================================================
FILE: app/letsencrypt_service_data.tmpl
================================================
#!/bin/bash
# shellcheck disable=SC2034
{{- $DEFAULT_ACMESH_DNS_API_CONFIG := fromYaml (coalesce $.Env.ACMESH_DNS_API_CONFIG "") }}
{{- if $DEFAULT_ACMESH_DNS_API_CONFIG }}
    {{- "\n" }}declare -A DEFAULT_ACMESH_DNS_API_CONFIG=(
    {{- range $key, $value := $DEFAULT_ACMESH_DNS_API_CONFIG }}
        {{- "\n\t" }}['{{ $key }}']='{{ $value }}'
    {{- end }}
    {{- "\n" }})
{{- end }}


LETSENCRYPT_CONTAINERS=(
{{ $orderedContainers := sortObjectsByKeysDesc $ "Created" }}
{{ range $_, $container := whereExist $orderedContainers "Env.LETSENCRYPT_HOST" }}
    {{ if trim $container.Env.LETSENCRYPT_HOST }}
        {{ if parseBool (coalesce $container.Env.LETSENCRYPT_SINGLE_DOMAIN_CERTS "false") }}
            {{/* Explicit per-domain splitting of the certificate */}}
            {{ range $host := split $container.Env.LETSENCRYPT_HOST "," }}
                {{ $host := trim $host }}
                {{- "\n\t" }}'{{ printf "%.12s" $container.ID }}_{{ sha1 $host }}' # {{ $container.Name }}, created at {{ $container.Created }}
            {{ end }}
        {{ else }}
            {{/* Default: multi-domain (SAN) certificate */}}
            {{- "\n\t" }}'{{ printf "%.12s" $container.ID }}' # {{ $container.Name }}, created at {{ $container.Created }}
        {{ end }}
    {{ end }}
{{ end }}
)

{{ range $hosts, $containers := groupBy $ "Env.LETSENCRYPT_HOST" }}
    {{ $hosts := trimSuffix "," $hosts }}
    {{ range $container := $containers }}
        {{/* Trim spaces and set empty values on per-container environment variables */}}
        {{ $KEYSIZE := trim (coalesce $container.Env.LETSENCRYPT_KEYSIZE "") }}
        {{ $STAGING := trim (coalesce $container.Env.LETSENCRYPT_TEST "") }}
        {{ $EMAIL := trim (coalesce $container.Env.LETSENCRYPT_EMAIL "") }}
        {{ $CA_URI := trim (coalesce $container.Env.ACME_CA_URI "") }}
        {{ $ACME_CHALLENGE := trim (coalesce $container.Env.ACME_CHALLENGE "") }}
        {{ $ACMESH_DNS_API_CONFIG := fromYaml (coalesce $container.Env.ACMESH_DNS_API_CONFIG "") }}
        {{ $PREFERRED_CHAIN := trim (coalesce $container.Env.ACME_PREFERRED_CHAIN "") }}
        {{ $OCSP := trim (coalesce $container.Env.ACME_OCSP "") }}
        {{ $EAB_KID := trim (coalesce $container.Env.ACME_EAB_KID "") }}
        {{ $EAB_HMAC_KEY := trim (coalesce $container.Env.ACME_EAB_HMAC_KEY "") }}
        {{ $ZEROSSL_API_KEY := trim (coalesce $container.Env.ZEROSSL_API_KEY "") }}
        {{ $RESTART_CONTAINER := trim (coalesce $container.Env.LETSENCRYPT_RESTART_CONTAINER "") }}
        {{ $PRE_HOOK := trim (coalesce $container.Env.ACME_PRE_HOOK "") }}
        {{ $POST_HOOK := trim (coalesce $container.Env.ACME_POST_HOOK "") }}
        {{ $cid := printf "%.12s" $container.ID }}
        {{- "\n" }}# Container {{ $cid }} ({{ $container.Name }})
        {{ if parseBool (coalesce $container.Env.LETSENCRYPT_SINGLE_DOMAIN_CERTS "false") }}
            {{/* Explicit per-domain splitting of the certificate */}}
            {{ range $host := split $hosts "," }}
                {{ $host := trim $host }}
                {{ $host := trimSuffix "." $host }}
                {{ $hostHash := sha1 $host }}
                {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_HOST=('{{ $host }}')
                {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_KEYSIZE="{{ $KEYSIZE }}"
                {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_TEST="{{ $STAGING }}"
                {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_EMAIL="{{ $EMAIL }}"
                {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_CA_URI="{{ $CA_URI }}"
                {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_CHALLENGE="{{ $ACME_CHALLENGE }}"
                {{- if $ACMESH_DNS_API_CONFIG }}
                    {{- "\n" }}declare -A ACMESH_{{ $cid }}_{{ $hostHash }}_DNS_API_CONFIG=(
                    {{- range $key, $value := $ACMESH_DNS_API_CONFIG }}
                        {{- "\n\t" }}['{{ $key }}']='{{ $value }}'
                    {{- end }}
                    {{- "\n" }})
                {{- end }}
                {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_PREFERRED_CHAIN="{{ $PREFERRED_CHAIN }}"
                {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_OCSP="{{ $OCSP }}"
                {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_EAB_KID="{{ $EAB_KID }}"
                {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_EAB_HMAC_KEY="{{ $EAB_HMAC_KEY }}"
                {{- "\n" }}ZEROSSL_{{ $cid }}_{{ $hostHash }}_API_KEY="{{ $ZEROSSL_API_KEY }}"
                {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_RESTART_CONTAINER="{{ $RESTART_CONTAINER }}"
                {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_PRE_HOOK="{{ $PRE_HOOK }}"
                {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_POST_HOOK="{{ $POST_HOOK }}"
            {{ end }}
        {{ else }}
            {{/* Default: multi-domain (SAN) certificate */}}
            {{- "\n" }}LETSENCRYPT_{{ $cid }}_HOST=( 
                    {{- range $host := split $hosts "," }}
                        {{- $host := trim $host }}
                        {{- $host := trimSuffix "." $host }}
                        {{- "\n\t" }}'{{ $host }}'
                    {{- end }}
            {{- "\n" }})
            {{- "\n" }}LETSENCRYPT_{{ $cid }}_KEYSIZE="{{ $KEYSIZE }}"
            {{- "\n" }}LETSENCRYPT_{{ $cid }}_TEST="{{ $STAGING }}"
            {{- "\n" }}LETSENCRYPT_{{ $cid }}_EMAIL="{{ $EMAIL }}"
            {{- "\n" }}ACME_{{ $cid }}_CA_URI="{{ $CA_URI }}"
            {{- "\n" }}ACME_{{ $cid }}_CHALLENGE="{{ $ACME_CHALLENGE }}"
            {{- if $ACMESH_DNS_API_CONFIG }}
                {{- "\n" }}declare -A ACMESH_{{ $cid }}_DNS_API_CONFIG=(
                {{- range $key, $value := $ACMESH_DNS_API_CONFIG }}
                    {{- "\n\t" }}['{{ $key }}']='{{ $value }}'
                {{- end }}
                {{- "\n" }})
            {{- end }}
            {{- "\n" }}ACME_{{ $cid }}_PREFERRED_CHAIN="{{ $PREFERRED_CHAIN }}"
            {{- "\n" }}ACME_{{ $cid }}_OCSP="{{ $OCSP }}"
            {{- "\n" }}ACME_{{ $cid }}_EAB_KID="{{ $EAB_KID }}"
            {{- "\n" }}ACME_{{ $cid }}_EAB_HMAC_KEY="{{ $EAB_HMAC_KEY }}"
            {{- "\n" }}ZEROSSL_{{ $cid }}_API_KEY="{{ $ZEROSSL_API_KEY }}"
            {{- "\n" }}LETSENCRYPT_{{ $cid }}_RESTART_CONTAINER="{{ $RESTART_CONTAINER }}"
            {{- "\n" }}ACME_{{ $cid }}_PRE_HOOK="{{ $PRE_HOOK }}"
            {{- "\n" }}ACME_{{ $cid }}_POST_HOOK="{{ $POST_HOOK }}"
        {{ end }}
    {{ end }}
{{ end }}


================================================
FILE: app/nginx_location.conf
================================================
location ^~ /.well-known/acme-challenge/ {
    auth_basic off;
    auth_request off;
    allow all;
    root /usr/share/nginx/html;
    try_files $uri =404;
    break;
}


================================================
FILE: app/signal_le_service
================================================
#!/bin/bash

# Using busybox pkill
pkill -USR1 -f /app/letsencrypt_service


================================================
FILE: app/start.sh
================================================
#!/bin/bash

# SIGTERM-handler
term_handler() {
    [[ -n "$docker_gen_pid" ]] && kill "$docker_gen_pid"
    [[ -n "$letsencrypt_service_pid" ]] && kill "$letsencrypt_service_pid"

    # shellcheck source=functions.sh
    source /app/functions.sh
    remove_all_location_configurations
    remove_all_standalone_configurations

    exit 0
}

trap 'term_handler' INT QUIT TERM

/app/letsencrypt_service &
letsencrypt_service_pid=$!

wait_default="5s:20s"
DOCKER_GEN_WAIT="${DOCKER_GEN_WAIT:-$wait_default}"
docker-gen -watch -notify '/app/signal_le_service' -wait "$DOCKER_GEN_WAIT" /app/letsencrypt_service_data.tmpl /app/letsencrypt_service_data &
docker_gen_pid=$!

# wait "indefinitely"
while [[ -e /proc/$docker_gen_pid ]]; do
    wait $docker_gen_pid # Wait for any signals or end of execution of docker-gen
done

# Stop container properly
term_handler


================================================
FILE: docs/Advanced-usage.md
================================================
## Advanced usage (with the nginx and docker-gen containers)

**nginx-proxy** can also be run as two separate containers using the [nginx-proxy/**docker-gen**](https://github.com/nginx-proxy/docker-gen) image and the official [**nginx**](https://hub.docker.com/_/nginx/) image. You may want to do this to prevent having the docker socket bound to a publicly exposed container service (ie avoid mounting the docker socket in the nginx exposed container).

Please read and try [basic usage](./Basic-usage.md), and **validate that you have a working two containers setup** before using the three containers setup. In addition to the steps described there, running **nginx-proxy** as two separate containers with **acme-companion** requires the following:

1) Download and mount the template file [nginx.tmpl](https://github.com/nginx-proxy/nginx-proxy/blob/main/nginx.tmpl) into the **docker-gen** container. You can get the nginx.tmpl file with a command like:

```
curl https://raw.githubusercontent.com/nginx-proxy/nginx-proxy/main/nginx.tmpl > /path/to/nginx.tmpl
```

2) Use the `com.github.nginx-proxy.docker-gen` label on the **docker-gen** container, or explicitly set the `NGINX_DOCKER_GEN_CONTAINER` environment variable on the **acme-companion** container to the name or id of the **docker-gen** container (we'll use the later method in the example).

3) Declare `/etc/nginx/conf.d` as a volume on the nginx container so that it can be shared with the **docker-gen** container.

Example:

### Step 1 - nginx

* Start nginx [(official image)](https://hub.docker.com/_/nginx/) with the required volumes:

```shell
$ docker run --detach \
    --name nginx-proxy \
    --publish 80:80 \
    --publish 443:443 \
    --volume conf:/etc/nginx/conf.d  \
    --volume html:/usr/share/nginx/html \
    --volume certs:/etc/nginx/certs \
    nginx
```

### Step 2 - docker-gen

* Start the **docker-gen** container with the shared volumes (with `--volume-from`), the template file and the docker socket:

```shell
$ docker run --detach \
    --name nginx-proxy-gen \
    --volumes-from nginx-proxy \
    --volume /path/to/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro \
    --volume /var/run/docker.sock:/tmp/docker.sock:ro \
    nginxproxy/docker-gen \
    -notify-sighup nginx-proxy -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
```

Note that you must pass the exact name of the **nginx** container to **docker-gen** `-notify-sighup` argument (here `nginx-proxy`).


### Step 3 - acme-companion

* Start the **acme-companion** container with the `NGINX_DOCKER_GEN_CONTAINER` environment variable correctly set:

```shell
$ docker run --detach \
    --name nginx-proxy-acme \
    --volumes-from nginx-proxy \
    --volume /var/run/docker.sock:/var/run/docker.sock:ro \
    --volume acme:/etc/acme.sh \
    --env "NGINX_DOCKER_GEN_CONTAINER=nginx-proxy-gen" \
    --env "DEFAULT_EMAIL=mail@yourdomain.tld" \
    nginxproxy/acme-companion
```

### Step 4 - proxyed container(s)

* Once the three containers are up, start any containers to be proxied as described in [basic usage](./Basic-usage.md).

```shell
$ docker run --detach \
    --name your-proxyed-app \
    --env "VIRTUAL_HOST=subdomain.yourdomain.tld" \
    --env "LETSENCRYPT_HOST=subdomain.yourdomain.tld" \
    nginx
```

If you are experiencing issues with this setup, fall back to the [basic setup](./Basic-usage.md). The advanced setup is not meant to be obligatory.


================================================
FILE: docs/Basic-usage.md
================================================
## Basic usage (with the nginx-proxy container)

Two writable volumes must be declared on the **nginx-proxy** container so that they can be shared with the **acme-companion** container:

* `/etc/nginx/certs` to store certificates and private keys (readonly for the **nginx-proxy** container).
* `/usr/share/nginx/html` to write `HTTP-01` challenge files.

Additionally, a third volume must be declared on the **acme-companion** container to store `acme.sh` configuration and state: `/etc/acme.sh`.

Please also read the doc about [data persistence](./Persistent-data.md).

Example of use:

### Step 1 - nginx-proxy

Start **nginx-proxy** with the two additional volumes declared:

```shell
$ docker run --detach \
    --name nginx-proxy \
    --publish 80:80 \
    --publish 443:443 \
    --volume certs:/etc/nginx/certs \
    --volume html:/usr/share/nginx/html \
    --volume /var/run/docker.sock:/tmp/docker.sock:ro \
    nginxproxy/nginx-proxy
```

Binding the host docker socket (`/var/run/docker.sock`) inside the container to `/tmp/docker.sock` is a requirement of **nginx-proxy**.

### Step 2 - acme-companion

Start the **acme-companion** container, getting the volumes from **nginx-proxy** with `--volumes-from`:

```shell
$ docker run --detach \
    --name nginx-proxy-acme \
    --volumes-from nginx-proxy \
    --volume /var/run/docker.sock:/var/run/docker.sock:ro \
    --volume acme:/etc/acme.sh \
    --env "DEFAULT_EMAIL=mail@yourdomain.tld" \
    nginxproxy/acme-companion
```

The host docker socket has to be bound inside this container too, this time to `/var/run/docker.sock`.

Albeit **optional**, it is **recommended** to provide a valid default email address through the `DEFAULT_EMAIL` environment variable, so that Let's Encrypt can warn you about expiring certificates and allow you to recover your account.

### Step 3 - proxyed container(s)

Once both **nginx-proxy** and **acme-companion** containers are up and running, start any container you want proxyed with environment variables `VIRTUAL_HOST` and `LETSENCRYPT_HOST` both set to the domain(s) your proxyed container is going to use. Multiple hosts can be separated using commas.

[`VIRTUAL_HOST`](https://github.com/nginx-proxy/nginx-proxy#usage) control proxying by **nginx-proxy** and `LETSENCRYPT_HOST` control certificate creation and SSL enabling by **acme-companion**.

Certificates will only be issued for containers that have both `VIRTUAL_HOST` and `LETSENCRYPT_HOST` variables set to domain(s) that correctly resolve to the host, provided the host is publicly reachable.

```shell
$ docker run --detach \
    --name your-proxyed-app \
    --env "VIRTUAL_HOST=subdomain.yourdomain.tld" \
    --env "LETSENCRYPT_HOST=subdomain.yourdomain.tld" \
    nginx
```

The containers being proxied must expose the port to be proxied, either by using the `EXPOSE` directive in their Dockerfile or by using the `--expose` flag to `docker run` or `docker create`.

If the proxyed container listen on and expose another port than the default `80`, you can force **nginx-proxy** to use this port with the [`VIRTUAL_PORT`](https://github.com/nginx-proxy/nginx-proxy#multiple-ports) environment variable.

Example using [Grafana](https://hub.docker.com/r/grafana/grafana/) (expose and listen on port 3000):

```shell
$ docker run --detach \
    --name grafana \
    --env "VIRTUAL_HOST=othersubdomain.yourdomain.tld" \
    --env "VIRTUAL_PORT=3000" \
    --env "LETSENCRYPT_HOST=othersubdomain.yourdomain.tld" \
    --env "LETSENCRYPT_EMAIL=mail@yourdomain.tld" \
    grafana/grafana
```

Repeat [Step 3](#step-3---proxyed-containers) for any other container you want to proxy.


================================================
FILE: docs/Container-configuration.md
================================================
## Optional container environment variables for custom configuration.

* `ACME_CA_URI` - Directory URI for the CA ACME API endpoint (defaults to ``https://acme-v02.api.letsencrypt.org/directory``).

If you set this environment variable value to `https://acme-staging-v02.api.letsencrypt.org/directory` the container will obtain its certificates from Let's Encrypt test API endpoint that don't have the [5 certs/week/domain limit](https://letsencrypt.org/docs/rate-limits/) (but are not trusted by browsers).

For example

```bash
$ docker run --detach \
    --name nginx-proxy-acme \
    --volumes-from nginx-proxy \
    --volume /var/run/docker.sock:/var/run/docker.sock:ro \
    --volume certs:/etc/nginx/certs:rw \
    --volume acme:/etc/acme.sh \
    --env "ACME_CA_URI=https://acme-staging-v02.api.letsencrypt.org/directory" \
    nginxproxy/acme-companion
```
You can also create test certificates per container (see [Test certificates](./Let's-Encrypt-and-ACME.md#test-certificates))

* `DEBUG` - Set it to `1` to enable debugging of the entrypoint script and generation of LetsEncrypt certificates, which could help you pin point any configuration issues.

* `RENEW_PRIVATE_KEYS` - Set it to `false` to make `acme.sh` reuse previously generated private key for each certificate instead of creating a new one on certificate renewal. Reusing private keys can help if you intend to use [HPKP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning), but please note that HPKP has been deprecated by Google's Chrome and that it is therefore strongly discouraged to use it at all.

* `DHPARAM_BITS` - Change the key size of the RFC7919 Diffie-Hellman group used by the container from the default value of 4096 bits. Supported values are `2048`, `3072` and `4096`. The DH group file will be located in the container at `/etc/nginx/certs/dhparam.pem`. Mounting a different `dhparam.pem` file at that location will override the RFC7919 group creation by the acme-companion container. **COMPATIBILITY WARNING**: some older clients (like Java 6 and 7) do not support DH keys with over 1024 bits. In order to support these clients, you must provide your own `dhparam.pem`.

* `DHPARAM_SKIP` - Set it to `true` to disable the Diffie-Hellman group creation by the container entirely.

* `CA_BUNDLE` - This is a test only variable [for use with Pebble](https://github.com/letsencrypt/pebble#avoiding-client-https-errors). It changes the trusted root CA used by `acme.sh`, from the default Alpine trust store to the CA bundle file located at the provided path (inside the container). Do **not** use it in production unless you are running your own ACME CA.

* `CERTS_UPDATE_INTERVAL` - 3600 seconds by default, this defines how often the container will check if the certificates require update.

* `ACME_PRE_HOOK` - The provided command will be run before every certificate issuance. The action is limited to the commands available inside the **acme-companion** container. For example `--env "ACME_PRE_HOOK=echo 'start'"`. For more information see [Pre- and Post-Hook](./Hooks.md)

* `ACME_POST_HOOK` - The provided command will be run after every certificate issuance. The action is limited to the commands available inside the **acme-companion** container. For example `--env "ACME_POST_HOOK=echo 'end'"`. For more information see [Pre- and Post-Hook](./Hooks.md)

* `DEFAULT_RENEW` - 60 days by default, this defines the number of days between certificate renewals attempts. For certificates issued by certain Certificate Authorities, such as Buypass, which have a lifespan of 180 days, it may be advisable to initiate the renewal process on day 170 rather than the default day 60. See [BuyPass.com CA](https://github.com/acmesh-official/acme.sh/wiki/BuyPass.com-CA) for more detail.

* `ACME_HTTP_CHALLENGE_LOCATION` - Previously **acme-companion** automatically added the ACME HTTP challenge location to the nginx configuration through files generated in `/etc/nginx/vhost.d`. Recent versions of **nginx-proxy** (>= `1.6`) already include the required location configuration, which remove the need for **acme-companion** to attempt to dynamically add them. If you're running and older version of **nginx-proxy** (or **docker-gen** with an older version of the `nginx.tmpl` file), you can re-enable this behaviour by setting `ACME_HTTP_CHALLENGE_LOCATION` to `true`.

* `RELOAD_NGINX_ONLY_ONCE` - The companion reload nginx configuration after every new or renewed certificate. Previously this was done only once per service loop, at the end of the loop (this was causing delayed availability of HTTPS enabled application when multiple new certificates where requested at once, see [issue #1147](https://github.com/nginx-proxy/acme-companion/issues/1147)). You can restore the previous behaviour if needed by setting the environment variable `RELOAD_NGINX_ONLY_ONCE` to `true`.

* `DOCKER_CONTAINER_FILTERS` -  You can filter which containers are considered by acme-companion by using the `DOCKER_CONTAINER_FILTERS` environment variable (by default, acme-companion will consider all running containers). It takes a comma separated list of `key=value` pairs. For example, setting `DOCKER_CONTAINER_FILTERS` environment variable to `network=mynetwork` will cause acme-companion to consider only containers connected to the `mynetwork` network. See the [Docker CLI documentation](https://docs.docker.com/reference/cli/docker/container/ls/#filter) for details on available filters.


================================================
FILE: docs/Container-utilities.md
================================================
The container provide the following utilities (replace `nginx-proxy-acme` with the name or ID of your **acme-companion** container when executing the commands):

### Force certificates renewal
If needed, you can force a running **acme-companion** container to renew all certificates that are currently in use with the following command:

```bash
$ docker exec nginx-proxy-acme /app/force_renew
```

### Manually trigger the service loop
You can trigger the execution of the service loop before the hourly execution with:

```bash
$ docker exec nginx-proxy-acme /app/signal_le_service
```
Unlike the previous command, this won't force renewal of certificates that don't need to be renewed.

### Show certificates informations
To display informations about your existing certificates, use the following command:

```bash
$ docker exec nginx-proxy-acme /app/cert_status
```

================================================
FILE: docs/Docker-Compose.md
================================================
## Usage with Docker Compose

As stated by its repository, [Docker Compose](https://github.com/docker/compose) is a tool for defining and running multi-container Docker applications using a single _Compose file_. This Wiki page is not meant to be a definitive reference on how to run **nginx-proxy** and **acme-companion** with Docker Compose, as the number of possible setups is quite extensive and they can't be all covered.

### Before your start

Be sure to be familiar with both the [basic](./Basic-usage.md) and [advanced](./Advanced-usage.md) non compose setups, and Docker Compose usage.

Please read [getting container IDs](./Getting-containers-IDs.md) and be aware that the `--volumes-from method` is **only** available on compose file version 2.

The following examples are minimal, clean starting points using compose file version 2. Again they are not intended as a definitive reference.

The use of named containers and volume is not required but helps keeping everything clear and organized.

### Two containers example

```yaml
services:
  nginx-proxy:
    image: nginxproxy/nginx-proxy
    container_name: nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      # The vhost and conf volumes are only required
      # if you plan to obtain standalone certificates
      # - vhost:/etc/nginx/vhost.d
      # - conf:/etc/nginx/conf.d
      - html:/usr/share/nginx/html
      - certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro

  acme-companion:
    image: nginxproxy/acme-companion
    container_name: nginx-proxy-acme
    environment:
      - DEFAULT_EMAIL=mail@yourdomain.tld
    volumes_from:
      - nginx-proxy
    volumes:
      - certs:/etc/nginx/certs:rw
      - acme:/etc/acme.sh
      - /var/run/docker.sock:/var/run/docker.sock:ro

#networks:
#    default:
#        name: nginx-proxy

volumes:
  # vhost:
  # conf:
  html:
  certs:
  acme:
```

### Three containers example

```yaml
services:
  nginx-proxy:
    image: nginx:alpine
    container_name: nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      # The vhost volume is only required if you
      # plan to obtain standalone certificates
      # - vhost:/etc/nginx/vhost.d
      - conf:/etc/nginx/conf.d
      - html:/usr/share/nginx/html
      - certs:/etc/nginx/certs:ro

  docker-gen:
    image: nginxproxy/docker-gen
    container_name: nginx-proxy-gen
    command: -notify-sighup nginx-proxy -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
    volumes_from:
      - nginx-proxy
    volumes:
      - /path/to/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
    labels:
      - "com.github.nginx-proxy.docker-gen"

  acme-companion:
    image: nginxproxy/acme-companion
    container_name: nginx-proxy-acme
    environment:
      - DEFAULT_EMAIL=mail@yourdomain.tld
    volumes_from:
      - nginx-proxy
    volumes:
      - certs:/etc/nginx/certs:rw
      - acme:/etc/acme.sh
      - /var/run/docker.sock:/var/run/docker.sock:ro

#networks:
#    default:
#        name: nginx-proxy

volumes:
  # vhost:
  conf:
  html:
  certs:
  acme:
```

**Note:** don't forget to replace `/path/to/nginx.tmpl` with the actual path to the [`nginx.tmpl`](https://raw.githubusercontent.com/nginx-proxy/nginx-proxy/main/nginx.tmpl) file you downloaded.

### Other (external) examples

**Warning:** some of those examples might be outdated and not working properly with version >= `2.0` of this project.

If you want other examples how to use this container with Docker Compose, look at:

* [Nicolas Duchon's Examples](https://github.com/buchdag/letsencrypt-nginx-proxy-companion-compose) - with automated testing
* [Evert Ramos's Examples](https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion) - using docker-compose version '3'
* [Karl Fathi's Examples](https://github.com/fatk/docker-letsencrypt-nginx-proxy-companion-examples)
* [More examples from Karl](https://github.com/pixelfordinner/pixelcloud-docker-apps/tree/master/nginx-proxy)
* [George Ilyes' Examples](https://github.com/gilyes/docker-nginx-letsencrypt-sample)
* [Dmitry's simple docker-compose example](https://github.com/dmitrym0/simple-lets-encrypt-docker-compose-sample)
* [Radek's docker-compose jenkins example](https://github.com/dataminelab/docker-jenkins-nginx-letsencrypt)


================================================
FILE: docs/Getting-containers-IDs.md
================================================
## Getting nginx-proxy/nginx/docker-gen containers IDs

For **acme-companion** to work properly, it needs to know the ID of the **nginx**/**nginx-proxy** container (in both [two](./Basic-usage.md) and [three](./Advanced-usage.md) containers setups), plus the ID of the **docker-gen** container in a [three container setup](./Advanced-usage.md).

There are three methods to inform the **acme-companion** container of the **nginx**/**nginx-proxy** container ID:

* `label` method: add the label `com.github.nginx-proxy.nginx` to the **nginx**/**nginx-proxy** container.

* `environment variable` method: assign a fixed name to the **nginx**/**nginx-proxy** container with `container_name:` and set the environment variable `NGINX_PROXY_CONTAINER` to this name on the **acme-companion** container.

* `volumes_from` method. Using this method, the **acme-companion** container will get the **nginx**/**nginx-proxy** container ID from the volumes it got using the `volumes_from` option.

And two methods to inform the **acme-companion** container of the **docker-gen** container ID:

* `label` method: add the label `com.github.nginx-proxy.docker-gen` to the **docker-gen** container.

* `environment variable` method: assign a fixed name to the **docker-gen** container with `container_name:` and set the environment variable `NGINX_DOCKER_GEN_CONTAINER` to this name on the **acme-companion** container.

The methods for each container are sorted by order of precedence, meaning that if you use both the label and the volumes_from method, the ID of the **nginx**/**nginx-proxy** container that will be used will be the one found using the label. **There is no point in using more than one method at a time for either the nginx/nginx-proxy or docker-gen container beside potentially confusing yourself**.

The advantage the `label` methods have over the `environment variable` (and `volumes_from`) methods is enabling the use of the **acme-companion** in environments where containers names are dynamic, like in Swarm Mode or in Docker Cloud. However if you intend to do so, as upstream **docker-gen** lacks the ability to identify containers from labels, you'll need both to either use the two containers setup or to replace nginx-proxy/docker-gen with a fork that has this ability like [herlderco/docker-gen](https://github.com/helderco/docker-gen). Be advised that for now, this works to a very limited extent [(everything has to be on the same node)](https://github.com/nginx-proxy/acme-companion/pull/231#issuecomment-330624331).

#### Examples with three containers setups:

`label` method.
```
$ docker run --detach \
    [...]
    --label com.github.nginx-proxy.nginx \
    nginx

$ docker run --detach \
    [...]
    --label com.github.nginx-proxy.docker-gen \
    nginxproxy/docker-gen

$ docker run --detach \
    [...]
    nginxproxy/acme-companion
```

`environment variable` method
```
$ docker run --detach \
    [...]
    --name unique-container-name \
    nginx

$ docker run --detach \
    [...]
    --name another-unique-container-name \
    nginxproxy/docker-gen

$ docker run --detach \
    [...]
    --env NGINX_PROXY_CONTAINER=unique-container-name \
    --env NGINX_DOCKER_GEN_CONTAINER=another-unique-container-name \
    nginxproxy/acme-companion
```

`volumes_from` (**nginx**) + `label` (**docker-gen**) method
```
$ docker run --detach \
    [...]
    --name unique-container-name \
    nginx

$ docker run --detach \
    [...]
    --label com.github.nginx-proxy.docker-gen \
    nginxproxy/docker-gen

$ docker run --detach \
    [...]
    --volumes-from unique-container-name \
    nginxproxy/acme-companion
```

`volumes_from` (**nginx**) + `environment variable` (**docker-gen**) method
```
$ docker run --detach \
    [...]
    --name unique-container-name \
    nginx

$ docker run --detach \
    [...]
    --name another-unique-container-name \
    nginxproxy/docker-gen

$ docker run --detach \
    [...]
    --volumes-from unique-container-name \
    --env NGINX_DOCKER_GEN_CONTAINER=another-unique-container-name \
    nginxproxy/acme-companion
```


================================================
FILE: docs/Google-Trust-Services.md
================================================
## Google Trust Services

[Google Trust Service](https://pki.goog/) is an ACME CA with generous default quota and high ubiquity. 

Using Google Trust Services through an ACME client, like in this container, allows for unlimited 90 days and multi-domains (SAN) certificates.

### Activation

Google Trust Services support is activated when the `ACME_CA_URI` environment variable is set to the Google Trust Services ACME endpoint (`https://dv.acme-v02.api.pki.goog/directory`).

### Account

Google Trust Services requires the use of an externally bound account. First create a [Google Trust Services account](https://cloud.google.com/certificate-manager/docs/public-ca-tutorial#request-key-hmac):

- provide the pre-generated [EAB credentials](https://tools.ietf.org/html/rfc8555#section-7.3.4) using the `ACME_EAB_KID` and `ACME_EAB_HMAC_KEY` environment variables.

These variables can be set on the proxied containers or directly on the **acme-companion** container.


================================================
FILE: docs/Hooks.md
================================================
## Pre-Hooks and Post-Hooks

The Pre- and Post-Hooks of [acme.sh](https://github.com/acmesh-official/acme.sh/) are available through the corresponding environment variables. This allows to trigger actions just before and after certificates are issued (see [acme.sh documentation](https://github.com/acmesh-official/acme.sh/wiki/Using-pre-hook-post-hook-renew-hook-reloadcmd)).

If you set `ACME_PRE_HOOK` and/or `ACME_POST_HOOK` on the **acme-companion** container, **the actions for all certificates will be the same**. If you want specific actions to be run for specific certificates, set the `ACME_PRE_HOOK` / `ACME_POST_HOOK` environment variable(s) on the proxied container(s) instead. Default (on the **acme-companion** container) and per-container `ACME_PRE_HOOK` / `ACME_POST_HOOK` environment variables aren't combined : if both default and per-container variables are set for a given proxied container, the per-container variables will take precedence over the default.

If you want to run the same default hooks for most containers but not for some of them, you can set the `ACME_PRE_HOOK` / `ACME_POST_HOOK` environment variables to the Bash noop operator (ie, `ACME_PRE_HOOK=:`) on those containers.

#### Pre-Hook: `ACME_PRE_HOOK`
This command will be run before certificates are issued.

For example `echo 'start'` on the **acme-companion** container (setting a default Pre-Hook):
```shell
$ docker run --detach \
    --name nginx-proxy-acme \
    --volumes-from nginx-proxy \
    --volume /var/run/docker.sock:/var/run/docker.sock:ro \
    --volume acme:/etc/acme.sh \
    --env "DEFAULT_EMAIL=mail@yourdomain.tld" \
    --env "ACME_PRE_HOOK=echo 'start'" \
    nginxproxy/acme-companion
```

And on a proxied container (setting a per-container Pre-Hook):
```shell
$ docker run --detach \
    --name your-proxyed-app \
    --env "VIRTUAL_HOST=yourdomain.tld" \
    --env "LETSENCRYPT_HOST=yourdomain.tld" \
    --env "ACME_PRE_HOOK=echo 'start'" \
    nginx
```

#### Post-Hook: `ACME_POST_HOOK`
This command will be run after certificates are issued.

For example `echo 'end'` on the **acme-companion** container (setting a default Post-Hook):
```shell
$ docker run --detach \
    --name nginx-proxy-acme \
    --volumes-from nginx-proxy \
    --volume /var/run/docker.sock:/var/run/docker.sock:ro \
    --volume acme:/etc/acme.sh \
    --env "DEFAULT_EMAIL=mail@yourdomain.tld" \
    --env "ACME_POST_HOOK=echo 'end'" \
    nginxproxy/acme-companion
```

And on a proxied container (setting a per-container Post-Hook):
```shell
$ docker run --detach \
    --name your-proxyed-app \
    --env "VIRTUAL_HOST=yourdomain.tld" \
    --env "LETSENCRYPT_HOST=yourdomain.tld" \
    --env "ACME_POST_HOOK=echo 'start'" \
    nginx
```

#### Verification:
If you want to check wether the hook-command is delivered properly to [acme.sh](https://github.com/acmesh-official/acme.sh/), you should check `/etc/acme.sh/[EMAILADDRESS]/[DOMAIN]/[DOMAIN].conf`.
The variable `Le_PreHook` contains the Pre-Hook-Command base64 encoded.
The variable `Le_PostHook` contains the Post-Hook-Command base64 encoded.

#### Limitations
* The commands that can be used in the hooks are limited to the commands available inside the **acme-companion** container. `curl` and `wget` are available, therefore it is possible to communicate with tools outside the container via HTTP, allowing for complex actions to be implemented outside or in other containers.

#### Use-cases
* Changing some firewall rules just for the ACME authorization, so the ports 80 and/or 443 don't have to be publicly reachable at all time.
* Certificate "post processing" / conversion to another format.
* Monitoring.


================================================
FILE: docs/Invalid-authorizations.md
================================================
## Troubleshooting failing authorizations

The first two things to do in case of failing authorization are to run the **acme-companion** container with the environment variable `DEBUG=1` to enable the more detailed error messages, and to [request test certificates](./Let's-Encrypt-and-ACME.md#test-certificates) while troubleshooting the issue.

Common causes of of failing authorizations:

#### port `80` or `443` on your host are closed / filtered from the outside, possibly because of a misconfigured firewall.

Check your host `80` and `443` ports **from the outside** (as in from a host having a different public IP) with `nmap` or a similar tool.

#### your domain name does not resolve to your host IPv4 and/or IPv6.

Check that your domain name A (and AAAA, if present) records points to the correct adresses using `drill`, `dig` or `nslookup`.

#### your domain name advertise an AAAA (IPv6) record, but your host or your host's docker isn't actually reachable over IPv6.

Create a test nginx container on your host and try to reach it over both IPv4 and IPv6.

```bash
you@remotedockerhost$ docker run -d -p 80:80 nginx:alpine

you@localcomputer$ curl http://your.domain.tld
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
[...]
</html>

you@localcomputer$ curl -6 http://your.domain.tld
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
[...]
</html>
```

If you are unsure of your host/hosts's docker IPv6 connectivity, drop the AAAA record from your domain name and wait for the modification to propagate.

#### your domain name DNS provider answers incorrectly to CAA record requests.

Read https://letsencrypt.org/docs/caa/ and test with https://unboundtest.com/

#### the **nginx-proxy**/**nginx**/**docker-gen**/**acme-companion** containers were misconfigured.

Review [basic usage](./Basic-usage.md) or [advanced usage](./Advanced-usage.md), plus the [nginx-proxy documentation](https://github.com/nginx-proxy/nginx-proxy).

Pay special attention to the fact that the volumes **MUST** be shared between the different containers.

#### you forgot to set both `VIRTUAL_HOST` and `LETSENCRYPT_HOST` on the proxyed container.

Both are required. Every domain on `LETSENCRYPT_HOST`**must** be on `VIRTUAL_HOST`too.

#### you are using an outdated version of either **acme-companion** or the nginx.tmpl file (if running a 3 containers setup)

Pull `nginxproxy/acme-companion:latest` again and get the latest [latest nginx.tmpl](https://raw.githubusercontent.com/nginx-proxy/nginx-proxy/main/nginx.tmpl).


***


When not in debug mode, the challenge files are automatically cleaned up **after** the authorization process, wether it succeeded or failed, so trying to `curl` them from the outside if you didn't enable debug mode won't yeld any result. If don't want to enable debug mode, you can however create a test file inside the same folder and use it to test the challenge files reachability from the outside (over both IPv4 and IPv6 if you want to use the latter):

```
you@remotedockerhost$ docker exec your-le-container bash -c 'echo "Hello world!" > /usr/share/nginx/html/.well-known/acme-challenge/hello-world'

you@localcomputer$ curl http://yourdomain.tld/.well-known/acme-challenge/hello-world
Hello world!
you@localcomputer$ curl -6 http://yourdomain.tld/.well-known/acme-challenge/hello-world
Hello world!
```

If you have issues with the [advanced setup](./Advanced-usage.md), fall back to the [basic setup](./Basic-usage.md). The advanced setup is not meant to be obligatory.


================================================
FILE: docs/Let's-Encrypt-and-ACME.md
================================================
## Let's Encrypt / ACME

**NOTE on CAA**: Please ensure that your DNS provider answers correctly to CAA record requests. [If your DNS provider answer with an error, Let's Encrypt won't issue a certificate for your domain](https://letsencrypt.org/docs/caa/). Let's Encrypt do not require that you set a CAA record on your domain, just that your DNS provider answers correctly.

**NOTE on IPv6**: If the domain or sub domain you want to issue certificate for has an AAAA record set, Let's Encrypt will favor challenge validation over IPv6. [There is an IPv6 to IPv4 fallback in place but Let's Encrypt can't guarantee it'll work in every possible case](https://github.com/letsencrypt/boulder/issues/2770#issuecomment-340489871), so bottom line is **if you are not sure of both your host and your host's Docker reachability over IPv6, do not advertise an AAAA record** or LE challenge validation might fail.

As described on [basic usage](./Basic-usage.md), the `LETSENCRYPT_HOST` environment variables needs to be declared in each to-be-proxied application containers for which you want to enable SSL and create certificate. It most likely needs to be the same as the `VIRTUAL_HOST` variable and must resolve to your host (which has to be publicly reachable).

The following environment variables are optional and parametrize the way the Let's Encrypt client works.

### per proxyed container

#### DNS-01 ACME challenge

In order to switch to the DNS-01 ACME challenge, set the `ACME_CHALLENGE` environment variable to `DNS-01` on your acme-companion container. This will also require you to set the `ACMESH_DNS_API_CONFIG` environment variable to a JSON or YAML string containing the configuration for the DNS provider you are using. Inside the JSON or YAML string, the `DNS_API` property is always required and should be set to the name of the [acme.sh DNS API](https://github.com/acmesh-official/acme.sh/tree/3.1.2/dnsapi) you want to use.

The other properties required will depend on the DNS provider you are using. For more information on the required properties for each DNS provider, please refer to the [acme.sh documentation](https://github.com/acmesh-official/acme.sh/wiki/dnsapi) (please keep in mind that nginxproxy/acme-companion is using a fixed version of acme.sh, so the documentation might include DNS providers that are not yet available in the version used by this image).

Both `ACME_CHALLENGE` and `ACMESH_DNS_API_CONFIG` environment variables can also be set on the proxied application container, in which case they will override the values set on the acme-companion container, if any.

Not: if you do not plan on using the `HTTP-01` challenge at all, you won't need to share `/usr/share/nginx/html` between the **nginx-proxy** and **acme-companion** containers, and can remove this volume from both.

Example using [Cloudflare DNS](https://github.com/acmesh-official/acme.sh/blob/master/dnsapi/dns_cf.sh):
```console
docker run --detach \
    --name nginx-proxy-acme \
    --volume certs:/etc/nginx/certs \
    --volume acme:/etc/acme.sh \
    --volume /var/run/docker.sock:/var/run/docker.sock:ro \
    --env "DEFAULT_EMAIL=mail@yourdomain.tld" \
    --env "ACME_CHALLENGE=DNS-01" \
    --env "ACMESH_DNS_API_CONFIG={'DNS_API': 'dns_cf', 'CF_Key': 'yourCloudflareGlobalApiKey', 'CF_Email': 'yourCloudflareAccountEmail'}" \
    nginxproxy/acme-companion
```

Same example on a Docker compose file:
```yaml
services:
  # nginx proxy container omitted
    
  acme:
    image: nginxproxy/acme-companion
    container_name: nginx-proxy-acme
    volumes:
      - certs:/etc/nginx/certs
      - acme:/etc/acme.sh
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      DEFAULT_EMAIL: mail@yourdomain.tld
      ACME_CHALLENGE: DNS-01
      ACMESH_DNS_API_CONFIG: |-
        DNS_API: dns_cf
        CF_Key: yourCloudflareGlobalApiKey
        CF_Email: yourCloudflareAccountEmail
    
    # app container omitted

volumes:
  certs:
  acme:
```

If you experience issues with the DNS-01 ACME challenge, please try to get it working outside of the container before opening an issue. If you can't get it working outside of the container, please seek support on the [acme.sh repository](https://github.com/acmesh-official).

#### Multi-domains certificates

Specify multiple hosts with a comma delimiter to create multi-domains ([SAN](https://www.digicert.com/subject-alternative-name.htm)) certificates (the first domain in the list will be the base domain).

Example:

```shell
$ docker run --detach \
    --name your-proxyed-app \
    --env "VIRTUAL_HOST=yourdomain.tld,www.yourdomain.tld,anotherdomain.tld" \
    --env "LETSENCRYPT_HOST=yourdomain.tld,www.yourdomain.tld,anotherdomain.tld" \
    nginx
```

Let's Encrypt has a limit of [100 domains per certificate](https://letsencrypt.org/fr/docs/rate-limits/), while Buypass limit is [15 domains per certificate](https://www.buypass.com/ssl/products/go-ssl-campaign).

#### Separate certificate for each domain

The example above will issue a single domain certificate for all the domains listed in the `LETSENCRYPT_HOST` environment variable. If you need to have a separate certificate for each of the domains, you can add set the `LETSENCRYPT_SINGLE_DOMAIN_CERTS` environment variable to `true`.

Example:

```shell
$ docker run --detach \
    --name your-proxyed-app \
    --env "VIRTUAL_HOST=yourdomain.tld,www.yourdomain.tld,anotherdomain.tld" \
    --env "LETSENCRYPT_HOST=yourdomain.tld,www.yourdomain.tld,anotherdomain.tld" \
    --env "LETSENCRYPT_SINGLE_DOMAIN_CERTS=true" \
    nginx
```

#### Automatic certificate renewal
Every hour (3600 seconds) the certificates are checked and per default every certificate that have been issued at least [60 days](https://github.com/acmesh-official/acme.sh/blob/f2d350002e7c387fad9777a42cf9befe34996c35/acme.sh#L61) ago is renewed. For Let's Encrypt certificates, that mean they will be renewed 30 days before expiration.

#### Contact address

The `LETSENCRYPT_EMAIL` environment variable must be a valid email and can be used to recover an account.

#### Private key size

The `LETSENCRYPT_KEYSIZE` environment variable determines the type and size of the requested key. Supported values are `2048`, `3072` and `4096` for RSA keys, and `ec-256` or `ec-384` for elliptic curve keys. The default is RSA 4096.  
To change the global default set the `DEFAULT_KEY_SIZE` environment variable on the **acme-companion** container to one of the supported values specified above.

#### OCSP stapling

The `ACME_OCSP` environment variable, when set to `true` on a proxied application container, will add the [OCSP Must-Staple extension](https://blog.apnic.net/2019/01/15/is-the-web-ready-for-ocsp-must-staple/) to the issued certificate. Please read about OCSP Must-Staple support in Nginx if you intend to use this feature (https://trac.nginx.org/nginx/ticket/812 and https://trac.nginx.org/nginx/ticket/1830)

#### Test certificates

The `LETSENCRYPT_TEST` environment variable, when set to `true` on a proxied application container, will create a test certificates that don't have the [5 certs/week/domain limits](https://letsencrypt.org/docs/rate-limits/) and are signed by an untrusted intermediate (they won't be trusted by browsers).

If you want to do this globally for all containers, set `ACME_CA_URI` on the **acme-companion** container as described in [Container configuration](./Container-configuration.md).

#### ACME CA URI

The `ACME_CA_URI` environment variable is used to set the ACME API endpoint from which the container's certificate(s) will be requested (defaults to ``https://acme-v02.api.letsencrypt.org/directory``).

#### Preferred chain

If the ACME CA provides multiple cert chain, you can use the `ACME_PREFERRED_CHAIN` environment variable to select one. See [`acme.sh --preferred-chain` documentation](https://github.com/acmesh-official/acme.sh/wiki/Preferred-Chain) for more info.

#### Container restart on cert renewal

The `LETSENCRYPT_RESTART_CONTAINER` environment variable, when set to `true` on an application container, will restart this container whenever the corresponding cert (`LETSENCRYPT_HOST`) is renewed. This is useful when certificates are directly used inside a container for other purposes than HTTPS (e.g. an FTPS server), to make sure those containers always use an up to date certificate.

#### Pre-Hook and Post-Hook

The `ACME_PRE_HOOK` and `ACME_POST_HOOK` let you use the [`acme.sh` Pre- and Post-Hooks feature](https://github.com/acmesh-official/acme.sh/wiki/Using-pre-hook-post-hook-renew-hook-reloadcmd) to run commands respectively before and after the container's certificate has been issued. For more information see [Pre- and Post-Hook](./Hooks.md)


### global (set on acme-companion container)

#### Default contact address

The `DEFAULT_EMAIL` variable must be a valid email and, when set on the **acme-companion** container, will be used as a fallback when no email address is provided using proxyed container's `LETSENCRYPT_EMAIL` environment variables. It is highly recommended to set this variable to a valid email address that you own.

#### Private key re-utilization

The `RENEW_PRIVATE_KEYS` environment variable, when set to `false` on the **acme-companion** container, will set `acme.sh` to reuse previously generated private key instead of generating a new one at renewal for all domains.

Reusing private keys can help if you intend to use [HPKP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning), but please note that HPKP has been deprecated by Google's Chrome and that it is therefore strongly discouraged to use it at all.

#### ACME accounts handling

- Use one `acme.sh` configuration directory (`--config-home`) per account email address.
- Each `acme.sh` configuration directory can hold several accounts on different ACME service providers. But only one per service provider.
- The `default` configuration directory holds the configuration for empty account email address.
- When in testing mode (`LETSENCRYPT_TEST=true`):
    1. The container will use the special purpose `staging` configuration directory.
    1. The directory URI is forced to The Let's Encrypt v2 staging one (`ACME_CA_URI` is ignored)
    2. The account email address is forced empty (`DEFAULT_EMAIL` and `LETSENCRYPT_EMAIL` are ignored)

#### Self signed default certificate

If you want **acme-companio** to create a self signed certificate as default certificate for **nginx-proxy**, you can set the `CREATE_DEFAULT_CERTIFICATE` environment variable to `true`. This will generate a self signed cert / key pair to `/etc/nginx/certs/default.crt` and `/etc/nginx/certs/default.key`, with `acme-companion` as Common Name. Please note that no future support is planned for this feature and it might be removed in a future release.


================================================
FILE: docs/Persistent-data.md
================================================
## Persistent data

### Named volumes (recommended)

When you follow instructions from Basic usage or Advanced usage, Docker will automatically create **named volumes** for every `--volume` / `-v` argument passed. Named volume will make it easy for you to mount the same persisted data even if you delete then re-create the container:

```shell
$ docker run -d \
    --name nginx-proxy \
    -p 80:80 \
    -p 443:443 \
    -v certs:/etc/nginx/certs \
    -v html:/usr/share/nginx/html \
    -v /var/run/docker.sock:/tmp/docker.sock:ro \
    nginxproxy/nginx-proxy

$ docker volume ls
DRIVER              VOLUME NAME
local               certs
local               html
```

### Anonymous volumes (not recommended)

If you don't prefix your volumes with a name, Docker will instead create **anonymous volumes** (volumes with a random name). Those volume persist after the container is deleted but aren't automatically re-mounted when you re-create the container. Their usage is **not recommended** as they don't provide any advantages over named volumes and make keeping tracks of what volume store which data way harder.

```shell
$ docker run -d \
    --name nginx-proxy \
    -p 80:80 \
    -p 443:443 \
    -v /etc/nginx/certs \
    -v /usr/share/nginx/html \
    -v /var/run/docker.sock:/tmp/docker.sock:ro \
    nginxproxy/nginx-proxy

$ docker volume ls
DRIVER              VOLUME NAME
local               287be3abd610e5566500d719ceb8b952952f12c9324ef02d05785d4ee9737ae9
local               f260f71fefadcdfc311d285d69151f2312915174d3fb1fab89949ec5ec871a54
```

### Host volumes

Alternatively, you might want to store the certificates on a local folder rather than letting Docker create and manage a volume for them. This is easily achieved by using a **host volume** (binding an absolute path on your host to the `/ect/nginx/certs` folder on your containers):

`-v /path/to/certificates:/etc/nginx/certs`

No matter the type of volume you choose, if you set them on the nginx-proxy or nginx container and use `--volumes_from` on the others containers, they will automatically be mounted inside the container to the path your first defined.

### Restraining other containers write permission

If you want to restrain the **nginx** and **docker-gen** processes to read only access on the certificates, you'll have to use different volume flags depending on the container.

Example with named volumes:

`-v certs:/etc/nginx/certs:ro` on the **nginx-proxy** or **nginx** + **docker-gen** container(s).

`-v certs:/etc/nginx/certs:rw` on the **acme-companion** container.

## Ownership & permissions of private and ACME account keys

By default, the **acme-companion** container will enforce the following ownership and permissions scheme on the files it creates and manage:

```
[drwxr-xr-x]  /etc/nginx/certs
├── [-rw-r--r-- root root]  dhparam.pem
├── [drwxr-xr-x root root]  domain.tld
│   ├── [-rw-r--r-- root root]  cert.pem
│   ├── [-rw-r--r-- root root]  chain.pem
│   ├── [-rw-r--r-- root root]  fullchain.pem
│   └── [-rw------- root root]  key.pem
├── [lrwxrwxrwx root root]  domain.tld.chain.pem -> ./domain.tld/chain.pem
├── [lrwxrwxrwx root root]  domain.tld.crt -> ./domain.tld/fullchain.pem
├── [lrwxrwxrwx root root]  domain.tld.dhparam.pem -> ./dhparam.pem
└── [lrwxrwxrwx root root]  domain.tld.key -> ./domain.tld/key.pem
```

This behavior can be customized using the following environment variable on the **acme-companion** container:

* `FILES_UID` - Set the user owning the files and folders managed by the container. The variable can be either a user name if this user exists inside the container or a user numeric ID. Default to `root` (user ID `0`).
* `FILES_GID` - Set the group owning the files and folders managed by the container. The variable can be either a group name if this group exists inside the container or a group numeric ID. Default to the same value as `FILES_UID`.
* `FILES_PERMS` - Set the permissions of the private keys. The variable must be a valid octal permission setting and defaults to `600`.
* `FOLDERS_PERMS` - Set the permissions of the folders managed by the container. The variable must be a valid octal permission setting and defaults to `755`.

For example, `FILES_UID=1000`, `FILES_PERMS=644` and `FOLDERS_PERMS=700` will result in the following:

```
[drwxr-xr-x]  /etc/nginx/certs
├── [-rw-r--r-- 1000 1000]  dhparam.pem
├── [drwx------ 1000 1000]  domain.tld
│   ├── [-rw-r--r-- 1000 1000]  cert.pem
│   ├── [-rw-r--r-- 1000 1000]  chain.pem
│   ├── [-rw-r--r-- 1000 1000]  fullchain.pem
│   └── [-rw-r--r-- 1000 1000]  key.pem
├── [lrwxrwxrwx 1000 1000]  domain.tld.chain.pem -> ./domain.tld/chain.pem
├── [lrwxrwxrwx 1000 1000]  domain.tld.crt -> ./domain.tld/fullchain.pem
├── [lrwxrwxrwx 1000 1000]  domain.tld.dhparam.pem -> ./dhparam.pem
└── [lrwxrwxrwx 1000 1000]  domain.tld.key -> ./domain.tld/key.pem
```


================================================
FILE: docs/README.md
================================================
#### Usage:

[Basic (two containers).](./Basic-usage.md)

[Advanced (three containers).](./Advanced-usage.md)

[Getting containers IDs](./Getting-containers-IDs.md)

[with Docker Compose](./Docker-Compose.md)

[Container utilities](./Container-utilities.md)

#### Additional configuration:

[Let's Encrypt / ACME](./Let's-Encrypt-and-ACME.md)

[Container configuration](./Container-configuration.md)

[Google Trust Services](./Google-Trust-Services.md)

[Persistent data](./Persistent-data.md)

[Standalone certificates](./Standalone-certificates.md)

[Zero SSL](./Zero-SSL.md)

[Pre-Hooks and Post-Hooks](./Hooks.md)

#### Troubleshooting:

[Invalid / failing authorizations](./Invalid-authorizations.md)


================================================
FILE: docs/Standalone-certificates.md
================================================
## Standalone certificates

You can generate certificate that are not tied to containers environment variable by mounting a user configuration file inside the container at `/app/letsencrypt_user_data`. This feature also require sharing the `/etc/nginx/vhost.d` and `/etc/nginx/conf.d` folder between the **nginx-proxy** and **acme-companion** container (and the **docker-gen** container if you are running a [three container setup](./Advanced-usage.md)):

```bash
$ docker run --detach \
    --name nginx-proxy \
    --publish 80:80 \
    --publish 443:443 \
    --volume certs:/etc/nginx/certs \
    --volume vhost:/etc/nginx/vhost.d \
    --volume conf:/etc/nginx/conf.d \
    --volume html:/usr/share/nginx/html \
    --volume /var/run/docker.sock:/tmp/docker.sock:ro \
    nginxproxy/nginx-proxy
```

```bash
$ docker run --detach \
    --name nginx-proxy-acme \
    --volumes-from nginx-proxy \
    --volume /var/run/docker.sock:/var/run/docker.sock:ro \
    --volume acme:/etc/acme.sh \
    --volume /path/to/your/config_file:/app/letsencrypt_user_data:ro \
    nginxproxy/acme-companion
```

The user configuration file is a collection of bash variables and array, and follows the syntax of the `/app/letsencrypt_service_data` file that get created by **docker-gen**.

### Required configuration parameters:

`LETSENCRYPT_STANDALONE_CERTS` : a bash array containing identifier(s) for you standalone certificate(s). Each element in the array has to be unique. Those identifiers are internal to the container process and won't ever be visible to the outside world or appear on your certificate.

`LETSENCRYPT_uniqueidentifier_HOST` : a bash array containing domain(s) that will be covered by the certificate corresponding to `uniqueidentifier`.

Each identifier in `LETSENCRYPT_STANDALONE_CERTS` must have its own corresponding `LETSENCRYPT_uniqueidentifier_HOST` array, where the string `uniqueidentifier` has to be identical to that identifier. 

**Minimal example generating a single certificate for a single domain:**

```bash
LETSENCRYPT_STANDALONE_CERTS=('uniqueidentifier')
LETSENCRYPT_uniqueidentifier_HOST=('yourdomain.tld')
```

**Example with multiple certificates and domains:**

```bash
LETSENCRYPT_STANDALONE_CERTS=('web' 'app' 'othersite')
LETSENCRYPT_web_HOST=('yourdomain.tld' 'www.yourdomain.tld')
LETSENCRYPT_app_HOST=('myapp.yourdomain.tld' 'myapp.yourotherdomain.tld' 'service.yourotherdomain.tld')
LETSENCRYPT_othersite_HOST=('yetanotherdomain.tld')
```

**Example using DNS-01 verification:**

In this example: `web` and `app` generate a certificate using the global/default configuration. However `othersite` will perform it's certificate verification using a specific DNS-01 API configuration.

```bash
LETSENCRYPT_STANDALONE_CERTS=('web' 'app' 'othersite')
LETSENCRYPT_web_HOST=('yourdomain.tld' 'www.yourdomain.tld')
LETSENCRYPT_app_HOST=('myapp.yourdomain.tld' 'myapp.yourotherdomain.tld' 'service.yourotherdomain.tld')
LETSENCRYPT_othersite_HOST=('yetanotherdomain.tld')

ACME_othersite_CHALLENGE=DNS-01
declare -A ACMESH_othersite_DNS_API_CONFIG=(
    ['DNS_API']='dns_cf'
    ['CF_Token']='<CLOUDFLARE_TOKEN>'
    ['CF_Account_ID']='<CLOUDFLARE_ACCOUNT_ID>'
    ['CF_Zone_ID']='<CLOUDFLARE_ZONE_ID>'
)
```

### Optional configuration parameters:

Single bash variables:

`LETSENCRYPT_uniqueidentifier_EMAIL` : must be a valid email and will be used by Let's Encrypt to warn you of impeding certificate expiration (should the automated renewal fail).

`LETSENCRYPT_uniqueidentifier_KEYSIZE` : determines the size of the requested private key. See [private key size](./Let's-Encrypt-and-ACME.md#private-key-size) for accepted values.

`LETSENCRYPT_uniqueidentifier_TEST` : if set to true, the corresponding certificate will be a test certificates: it won't have the 5 certs/week/domain limits and will be signed by an untrusted intermediate (ie it won't be trusted by browsers).

DNS-01 related variables:

`ACME_uniqueidentifier_CHALLENGE`: Defaults to HTTP-01. In order to switch to the DNS-01 ACME challenge set it to `DNS-01`

`ACMESH_uniqueidentifier_DNS_API_CONFIG`: Defaults to the values of DNS_API_CONFIG. However if you wish to specify a specific DNS-01 verification method on a particular standalone certificate. It must be defined as a bash associative array.

Example
```bash
declare -A ACMESH_alt_DNS_API_CONFIG=(
    ['DNS_API']='dns_cf'
    ['CF_Token']='<CLOUDFLARE_TOKEN>'
    ['CF_Account_ID']='<CLOUDFLARE_ACCOUNT_ID>'
    ['CF_Zone_ID']='<CLOUDFLARE_ZONE_ID>'
)
```

### Picking up changes to letsencrypt_user_data

The container does not actively watch the `/app/letsencrypt_user_data` file for changes.

Changes will either be picked up every hour when the service loop execute again, or by using `docker exec your-le-container-name-or-id /app/signal_le_service` to manually trigger the service loop execution.

### Proxying to something else than a Docker container

Please see the [**nginx-proxy** documentation](https://github.com/nginx-proxy/nginx-proxy#proxy-wide).

No support will be provided on the **acme-companion** repo for proxying related issues or questions.


================================================
FILE: docs/Zero-SSL.md
================================================
## Zero SSL

[Zero SSL](https://zerossl.com/) is an ACME CA that offer some advantages over Let's Encrypt:
- no staging endpoint and [no rate limiting on the production endpoint](https://zerossl.com/features/acme/).
- web based [management console](https://zerossl.com/features/console/) to keep track of your SSL certificates.

Using Zero SSL through an ACME client, like in this container, allows for unlimited 90 days and multi-domains (SAN) certificates.

### Activation

The Zero SSL support is activated when the `ACME_CA_URI` environment variable is set to the Zero SSL ACME endpoint (`https://acme.zerossl.com/v2/DV90`).

### Account

Unlike Let's Encrypt, Zero SSL requires the use of an email bound account. If you already created a Zero SSL account, you can either:

- provide pre-generated [EAB credentials](https://tools.ietf.org/html/rfc8555#section-7.3.4) using the `ACME_EAB_KID` and `ACME_EAB_HMAC_KEY` environment variables.
- provide your ZeroSSL API key using the `ZEROSSL_API_KEY` environment variable.

These variables can be set on the proxied containers or directly on the **acme-companion** container.

If you don't have a ZeroSSL account, you can let **acme-companion** create a Zero SSL account with the address provided in the `ACME_EMAIL` or `DEFAULT_EMAIL` environment variable. Note that the address that will be used must be a valid email address that you actually own.


================================================
FILE: install_acme.sh
================================================
#!/bin/bash

set -e

# Install git (required to fetch acme.sh)
apk --no-cache --virtual .acmesh-deps add git

# Get acme.sh ACME client source
mkdir /src
git -C /src clone https://github.com/acmesh-official/acme.sh.git
cd /src/acme.sh
if [[ "$ACMESH_VERSION" != "master" ]]; then
  git -c advice.detachedHead=false checkout "$ACMESH_VERSION"
fi

# Install acme.sh in /app
./acme.sh --install \
  --nocron \
  --auto-upgrade 0 \
  --home /app \
  --config-home /etc/acme.sh/default

# Make house cleaning
cd /
rm -rf /src
apk del .acmesh-deps


================================================
FILE: test/README.md
================================================
### acme-companion test suite

The test suite can be run locally on a Linux or macOS host.

To prepare the test setup:

```bash
git clone https://github.com/nginx-proxy/acme-companion.git
cd acme-companion
test/setup/setup-local.sh --setup
```

Then build the docker image and run the tests:

```bash
docker build -t nginxproxy/acme-companion .
test/run.sh nginxproxy/acme-companion
```

You can limit the test run to specific test(s) with the `-t` flag:

```bash
test/run.sh -t docker_api nginxproxy/acme-companion
```

When running the test suite, the standard output of each individual test is captured and compared to its expected-std-out.txt file. When developing or modifying a test, you can use the `-d` flag to disable the standard output capture by the test suite.

```bash
test/run.sh -d nginxproxy/acme-companion
```

To remove the test setup:

```bash
test/setup/setup-local.sh --teardown
```


================================================
FILE: test/config.sh
================================================
#!/bin/bash
set -e

globalTests+=(
	docker_api
	docker_api_legacy
	location_config
	certs_single
	certs_san
	certs_single_domain
	certs_standalone
	force_renew
	acme_accounts
	private_keys
	container_restart
	permissions_default
	permissions_custom
	symlinks
	acme_hooks
	certs_default_renew
	ocsp_must_staple
)

# The acme_eab test requires Pebble with a specific configuration
if [[ "$ACME_CA" == 'pebble' && "$PEBBLE_CONFIG" == 'pebble-config-eab.json' ]]; then
	globalTests+=(
		acme_eab
	)
fi


================================================
FILE: test/github_actions/containers-logs.sh
================================================
#!/bin/bash

bold_echo() {
  echo -e "\033[33;1m$1\033[0m"
}

if [[ -f "$GITHUB_WORKSPACE/test/github_actions/failed_tests.txt" ]]; then
  mapfile -t containers < "$GITHUB_WORKSPACE/test/github_actions/failed_tests.txt"
fi

containers+=("$NGINX_CONTAINER_NAME")
[[ $SETUP = "3containers" ]] && containers+=("$DOCKER_GEN_CONTAINER_NAME")
[[ $ACME_CA = "boulder" ]] && containers+=(boulder)
[[ $ACME_CA = "pebble" ]] && containers+=(pebble challtestserv)

for container in "${containers[@]}"; do
  bold_echo "Docker container output for $container"
  docker logs "$container"
  docker inspect "$container"
  if [[ "$container" == "acme_accounts" ]]; then
    bold_echo "Docker container output for ${container}_default"
    docker logs "${container}_default"
    docker inspect "${container}_default"
  fi
done


================================================
FILE: test/run.sh
================================================
#!/usr/bin/env bash
#shellcheck disable=SC2068,SC2206

# This file is adapted from the Docker official images test suite
# under Apache 2.0 License and as such includes of copy of the license
# https://github.com/docker-library/official-images/tree/master/test
#
#                               Apache License
#                         Version 2.0, January 2004
#                      http://www.apache.org/licenses/
#
# TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
#
# 1. Definitions.
#
#    "License" shall mean the terms and conditions for use, reproduction,
#    and distribution as defined by Sections 1 through 9 of this document.
#
#    "Licensor" shall mean the copyright owner or entity authorized by
#    the copyright owner that is granting the License.
#
#    "Legal Entity" shall mean the union of the acting entity and all
#    other entities that control, are controlled by, or are under common
#    control with that entity. For the purposes of this definition,
#    "control" means (i) the power, direct or indirect, to cause the
#    direction or management of such entity, whether by contract or
#    otherwise, or (ii) ownership of fifty percent (50%) or more of the
#    outstanding shares, or (iii) beneficial ownership of such entity.
#
#    "You" (or "Your") shall mean an individual or Legal Entity
#    exercising permissions granted by this License.
#
#    "Source" form shall mean the preferred form for making modifications,
#    including but not limited to software source code, documentation
#    source, and configuration files.
#
#    "Object" form shall mean any form resulting from mechanical
#    transformation or translation of a Source form, including but
#    not limited to compiled object code, generated documentation,
#    and conversions to other media types.
#
#    "Work" shall mean the work of authorship, whether in Source or
#    Object form, made available under the License, as indicated by a
#    copyright notice that is included in or attached to the work
#    (an example is provided in the Appendix below).
#
#    "Derivative Works" shall mean any work, whether in Source or Object
#    form, that is based on (or derived from) the Work and for which the
#    editorial revisions, annotations, elaborations, or other modifications
#    represent, as a whole, an original work of authorship. For the purposes
#    of this License, Derivative Works shall not include works that remain
#    separable from, or merely link (or bind by name) to the interfaces of,
#    the Work and Derivative Works thereof.
#
#    "Contribution" shall mean any work of authorship, including
#    the original version of the Work and any modifications or additions
#    to that Work or Derivative Works thereof, that is intentionally
#    submitted to Licensor for inclusion in the Work by the copyright owner
#    or by an individual or Legal Entity authorized to submit on behalf of
#    the copyright owner. For the purposes of this definition, "submitted"
#    means any form of electronic, verbal, or written communication sent
#    to the Licensor or its representatives, including but not limited to
#    communication on electronic mailing lists, source code control systems,
#    and issue tracking systems that are managed by, or on behalf of, the
#    Licensor for the purpose of discussing and improving the Work, but
#    excluding communication that is conspicuously marked or otherwise
#    designated in writing by the copyright owner as "Not a Contribution."
#
#    "Contributor" shall mean Licensor and any individual or Legal Entity
#    on behalf of whom a Contribution has been received by Licensor and
#    subsequently incorporated within the Work.
#
# 2. Grant of Copyright License. Subject to the terms and conditions of
#    this License, each Contributor hereby grants to You a perpetual,
#    worldwide, non-exclusive, no-charge, royalty-free, irrevocable
#    copyright license to reproduce, prepare Derivative Works of,
#    publicly display, publicly perform, sublicense, and distribute the
#    Work and such Derivative Works in Source or Object form.
#
# 3. Grant of Patent License. Subject to the terms and conditions of
#    this License, each Contributor hereby grants to You a perpetual,
#    worldwide, non-exclusive, no-charge, royalty-free, irrevocable
#    (except as stated in this section) patent license to make, have made,
#    use, offer to sell, sell, import, and otherwise transfer the Work,
#    where such license applies only to those patent claims licensable
#    by such Contributor that are necessarily infringed by their
#    Contribution(s) alone or by combination of their Contribution(s)
#    with the Work to which such Contribution(s) was submitted. If You
#    institute patent litigation against any entity (including a
#    cross-claim or counterclaim in a lawsuit) alleging that the Work
#    or a Contribution incorporated within the Work constitutes direct
#    or contributory patent infringement, then any patent licenses
#    granted to You under this License for that Work shall terminate
#    as of the date such litigation is filed.
#
# 4. Redistribution. You may reproduce and distribute copies of the
#    Work or Derivative Works thereof in any medium, with or without
#    modifications, and in Source or Object form, provided that You
#    meet the following conditions:
#
#    (a) You must give any other recipients of the Work or
#        Derivative Works a copy of this License; and
#
#    (b) You must cause any modified files to carry prominent notices
#        stating that You changed the files; and
#
#    (c) You must retain, in the Source form of any Derivative Works
#        that You distribute, all copyright, patent, trademark, and
#        attribution notices from the Source form of the Work,
#        excluding those notices that do not pertain to any part of
#        the Derivative Works; and
#
#    (d) If the Work includes a "NOTICE" text file as part of its
#        distribution, then any Derivative Works that You distribute must
#        include a readable copy of the attribution notices contained
#        within such NOTICE file, excluding those notices that do not
#        pertain to any part of the Derivative Works, in at least one
#        of the following places: within a NOTICE text file distributed
#        as part of the Derivative Works; within the Source form or
#        documentation, if provided along with the Derivative Works; or,
#        within a display generated by the Derivative Works, if and
#        wherever such third-party notices normally appear. The contents
#        of the NOTICE file are for informational purposes only and
#        do not modify the License. You may add Your own attribution
#        notices within Derivative Works that You distribute, alongside
#        or as an addendum to the NOTICE text from the Work, provided
#        that such additional attribution notices cannot be construed
#        as modifying the License.
#
#    You may add Your own copyright statement to Your modifications and
#    may provide additional or different license terms and conditions
#    for use, reproduction, or distribution of Your modifications, or
#    for any such Derivative Works as a whole, provided Your use,
#    reproduction, and distribution of the Work otherwise complies with
#    the conditions stated in this License.
#
# 5. Submission of Contributions. Unless You explicitly state otherwise,
#    any Contribution intentionally submitted for inclusion in the Work
#    by You to the Licensor shall be under the terms and conditions of
#    this License, without any additional terms or conditions.
#    Notwithstanding the above, nothing herein shall supersede or modify
#    the terms of any separate license agreement you may have executed
#    with Licensor regarding such Contributions.
#
# 6. Trademarks. This License does not grant permission to use the trade
#    names, trademarks, service marks, or product names of the Licensor,
#    except as required for reasonable and customary use in describing the
#    origin of the Work and reproducing the content of the NOTICE file.
#
# 7. Disclaimer of Warranty. Unless required by applicable law or
#    agreed to in writing, Licensor provides the Work (and each
#    Contributor provides its Contributions) on an "AS IS" BASIS,
#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
#    implied, including, without limitation, any warranties or conditions
#    of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
#    PARTICULAR PURPOSE. You are solely responsible for determining the
#    appropriateness of using or redistributing the Work and assume any
#    risks associated with Your exercise of permissions under this License.
#
# 8. Limitation of Liability. In no event and under no legal theory,
#    whether in tort (including negligence), contract, or otherwise,
#    unless required by applicable law (such as deliberate and grossly
#    negligent acts) or agreed to in writing, shall any Contributor be
#    liable to You for damages, including any direct, indirect, special,
#    incidental, or consequential damages of any character arising as a
#    result of this License or out of the use or inability to use the
#    Work (including but not limited to damages for loss of goodwill,
#    work stoppage, computer failure or malfunction, or any and all
#    other commercial damages or losses), even if such Contributor
#    has been advised of the possibility of such damages.
#
# 9. Accepting Warranty or Additional Liability. While redistributing
#    the Work or Derivative Works thereof, You may choose to offer,
#    and charge a fee for, acceptance of support, warranty, indemnity,
#    or other liability obligations and/or rights consistent with this
#    License. However, in accepting such obligations, You may act only
#    on Your own behalf and on Your sole responsibility, not on behalf
#    of any other Contributor, and only if You agree to indemnify,
#    defend, and hold each Contributor harmless for any liability
#    incurred by, or claims asserted against, such Contributor by reason
#    of your accepting any such warranty or additional liability.
#
# END OF TERMS AND CONDITIONS
#
# Copyright 2014 Docker, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -e

## Next twelve lines were added by nginxproxy/acme-companion
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
self="$(basename "$0")"
failed_tests=()

if [[ -z $GITHUB_ACTIONS ]] && [[ -f "$dir/local_test_env.sh" ]]; then
	# shellcheck source=/dev/null
	source "$dir/local_test_env.sh"
fi

# shellcheck source=./tests/test-functions.sh
source "$dir/tests/test-functions.sh"
## End of additional code

usage() {
	cat <<EOUSAGE

usage: $self [-t test ...] image:tag [...]
   ie: $self debian:stretch
       $self -t utc python:3
       $self -t utc python:3 -t python-hy

This script processes the specified Docker images to test their running
environments.
EOUSAGE
}

# arg handling
## Next nine lines were added or modified by nginxproxy/acme-companion
case "$(uname)" in
	Linux)
	opts="$(getopt -o 'hdkt:c:?' --long 'dry-run,help,test:,config:,keep-namespace' -- "$@" || { usage >&2 && false; })"
	;;

	Darwin)
	opts="$(getopt hdkt:c:? "$@" || { usage >&2 && false; })"
	;;
esac
## End of additional or modified code
eval set -- "$opts"

declare -A argTests=()
declare -a configs=()
dryRun=
keepNamespace=
while true; do
	flag=$1
	shift
	case "$flag" in
		## Next line was modified by nginxproxy/acme-companion
		--dry-run|-d) dryRun=1 && export DRY_RUN=1 ;;
		--help|-h|'-?') usage && exit 0 ;;
		--test|-t) argTests["$1"]=1 && shift ;;
		--config|-c) configs+=("$(readlink -f "$1")") && shift ;;
		## Next line was modified by nginxproxy/acme-companion
		--keep-namespace|-k) keepNamespace=1 ;;
		--) break ;;
		*)
			{
				echo "error: unknown flag: $flag"
				usage
			} >&2
			exit 1
			;;
	esac
done

if [ $# -eq 0 ]; then
	usage >&2
	exit 1
fi

# declare configuration variables
declare -a globalTests=()
declare -A testAlias=()
declare -A imageTests=()
declare -A globalExcludeTests=()
declare -A explicitTests=()

# if there are no user-specified configs, use the default config
if [ ${#configs} -eq 0 ]; then
	configs+=("$dir/config.sh")
fi

# load the configs
declare -A testPaths=()
for conf in "${configs[@]}"; do
	## Next two line were modified by nginxproxy/acme-companion
  # shellcheck source=./config.sh
	source "$conf"
	## End of modifications

	# Determine the full path to any newly-declared tests
	confDir="$(dirname "$conf")"

	for testName in ${globalTests[@]} ${imageTests[@]}; do
		[ "${testPaths[$testName]}" ] && continue

		if [ -d "$confDir/tests/$testName" ]; then
			# Test directory found relative to the conf file
			testPaths[$testName]="$confDir/tests/$testName"
		elif [ -d "$dir/tests/$testName" ]; then
			# Test directory found in the main tests/ directory
			testPaths[$testName]="$dir/tests/$testName"
		fi
	done
done

didFail=
for dockerImage in "$@"; do
	echo "testing $dockerImage"

	if ! docker inspect "$dockerImage" &> /dev/null; then
		echo $'\timage does not exist!'
		didFail=1
		continue
	fi

	repo="${dockerImage%:*}"
	tagVar="${dockerImage#*:}"
	#version="${tagVar%-*}"
	variant="${tagVar##*-}"

	## Irrelevant code was removed here by nginxproxy/acme-companion

	testRepo="$repo"
	if [ -z "$keepNamespace" ]; then
		testRepo="${testRepo##*/}"
	fi
	[ -z "${testAlias[$repo]}" ] || testRepo="${testAlias[$repo]}"

	explicitVariant=
	## Next four lines were modified by nginxproxy/acme-companion
	if [ "${explicitTests[:$variant]}" ] \
	|| [ "${explicitTests[$repo:$variant]}" ] \
	|| [ "${explicitTests[$testRepo:$variant]}" ]
	then
	## End of modified code
		explicitVariant=1
	fi

	testCandidates=()
	if [ -z "$explicitVariant" ]; then
		testCandidates+=( "${globalTests[@]}" )
	fi
	testCandidates+=(
		${imageTests[:$variant]}
	)
	if [ -z "$explicitVariant" ]; then
		testCandidates+=(
			${imageTests[$testRepo]}
		)
	fi
	testCandidates+=(
		${imageTests[$testRepo:$variant]}
	)
	if [ "$testRepo" != "$repo" ]; then
		if [ -z "$explicitVariant" ]; then
			testCandidates+=(
				${imageTests[$repo]}
			)
		fi
		testCandidates+=(
			${imageTests[$repo:$variant]}
		)
	fi

	tests=()
	for t in "${testCandidates[@]}"; do
		## Next line was modified by nginxproxy/acme-companion
		if [ ${#argTests[@]} -gt 0 ] && [ -z "${argTests[$t]}" ]; then
		## End of modified code
			# skipping due to -t
			continue
		fi

		## Next seven lines were modified by nginxproxy/acme-companion
		if [ -n "${globalExcludeTests[${testRepo}_$t]}" ] \
		|| [ -n "${globalExcludeTests[${testRepo}:${variant}_$t]}" ] \
		|| [ -n "${globalExcludeTests[:${variant}_$t]}" ] \
		|| [ -n "${globalExcludeTests[${repo}_$t]}" ] \
		|| [ -n "${globalExcludeTests[${repo}:${variant}_$t]}" ] \
		|| [ -n "${globalExcludeTests[:${variant}_$t]}" ]
		then
		## End of modified code
			# skipping due to exclude
			continue
		fi

		tests+=( "$t" )
	done

	currentTest=0
	totalTest="${#tests[@]}"
	for t in "${tests[@]}"; do
		(( currentTest+=1 ))
		echo -ne "\t'$t' [$currentTest/$totalTest]..."

		# run test against dockerImage here
		# find the script for the test
		scriptDir="${testPaths[$t]}"
		if [ -d "$scriptDir" ]; then
			script="$scriptDir/run.sh"
			## Next nine lines were modified or added by nginxproxy/acme-companion
			if [ -x "$script" ] && [ ! -d "$script" ]; then
				if [ $dryRun ]; then
					if "$script" "$dockerImage"; then
						echo 'passed'
					else
						echo 'failed'
						didFail=1
					fi
				else
				## End of modified / additional code
					if output="$("$script" "$dockerImage")"; then
						if [ -f "$scriptDir/expected-std-out.txt" ] && ! d="$(echo "$output" | diff -u "$scriptDir/expected-std-out.txt" - 2>/dev/null)"; then
							echo 'failed; unexpected output:'
							echo "$d"
							## Next line was added by nginxproxy/acme-companion
							failed_tests+=("$(basename "$scriptDir")")
							## End of additional code
							didFail=1
						else
							echo 'passed'
						fi
					else
						echo 'failed'
						## Next line was added by nginxproxy/acme-companion
						failed_tests+=("$(basename "$scriptDir")")
						## End of additional code
						didFail=1
					fi
				fi
			else
				echo "skipping"
				echo >&2 "error: $script missing, not executable or is a directory"
				didFail=1
				continue
			fi
		else
			echo "skipping"
			echo >&2 "error: unable to locate test '$t'"
			didFail=1
			continue
		fi
	done
done

if [ "$didFail" ]; then
	## Next five lines were added by nginxproxy/acme-companion
	if [[ $GITHUB_ACTIONS == 'true' ]]; then
		for test in "${failed_tests[@]}"; do
			echo "$test" >> "$dir/github_actions/failed_tests.txt"
		done
	fi
	## End of additional code
	exit 1
fi


================================================
FILE: test/setup/pebble/compose.yaml
================================================
services:
  pebble:
    image: "ghcr.io/letsencrypt/pebble:${PEBBLE_VERSION}"
    container_name: pebble
    volumes:
      - "./${PEBBLE_CONFIG}:/test/config/pebble-config.json"
    environment:
      - PEBBLE_VA_NOSLEEP=1
    command: -config /test/config/pebble-config.json -dnsserver 10.30.50.3:8053
    ports:
      - 14000:14000 # HTTPS ACME API
      - 15000:15000 # HTTPS Management API
    networks:
      acme_net:
        ipv4_address: 10.30.50.2

  challtestsrv:
    image: "ghcr.io/letsencrypt/pebble-challtestsrv:${PEBBLE_VERSION}"
    container_name: challtestserv
    command: -defaultIPv6 "" -defaultIPv4 10.30.50.3
    ports:
      - 8055:8055 # HTTP Management API
    networks:
      acme_net:
        ipv4_address: 10.30.50.3

networks:
  acme_net:
    name: acme_net
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 10.30.50.0/24


================================================
FILE: test/setup/pebble/pebble-config-eab.json
================================================
{
  "pebble": {
    "listenAddress": "0.0.0.0:14000",
    "managementListenAddress": "0.0.0.0:15000",
    "certificate": "test/certs/localhost/cert.pem",
    "privateKey": "test/certs/localhost/key.pem",
    "httpPort": 80,
    "tlsPort": 443,
    "ocspResponderURL": "",
    "externalAccountBindingRequired": true,
    "externalAccountMACKeys": {
      "kid-1": "zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W",
      "kid-2": "b10lLJs8l1GPIzsLP0s6pMt8O0XVGnfTaCeROxQM0BIt2XrJMDHJZBM5NuQmQJQH"
    },
    "retryAfter": {
      "authz": 3,
      "order": 5
    },
    "certificateValidityPeriod": 157766400
  }
}


================================================
FILE: test/setup/pebble/pebble-config.json
================================================
{
  "pebble": {
    "listenAddress": "0.0.0.0:14000",
    "managementListenAddress": "0.0.0.0:15000",
    "certificate": "test/certs/localhost/cert.pem",
    "privateKey": "test/certs/localhost/key.pem",
    "httpPort": 80,
    "tlsPort": 443,
    "ocspResponderURL": "",
    "externalAccountBindingRequired": false,
    "retryAfter": {
      "authz": 3,
      "order": 5
    },
    "certificateValidityPeriod": 157766400
  }
}


================================================
FILE: test/setup/pebble/setup-pebble.sh
================================================
#!/bin/bash

set -e

setup_pebble() {
    curl --silent --show-error https://raw.githubusercontent.com/letsencrypt/pebble/refs/tags/v2.10.0/test/certs/pebble.minica.pem > "${GITHUB_WORKSPACE}/pebble.minica.pem"
    cat "${GITHUB_WORKSPACE}/pebble.minica.pem"
    docker compose --file "${GITHUB_WORKSPACE}/test/setup/pebble/compose.yaml" up --detach
}

wait_for_pebble() {
    for endpoint in 'https://pebble:14000/dir' 'http://pebble-challtestsrv:8055'; do
        while ! curl --cacert "${GITHUB_WORKSPACE}/pebble.minica.pem" "$endpoint" >/dev/null 2>&1; do
            if [ $((i * 5)) -gt $((5 * 60)) ]; then
                echo "$endpoint was not available under 5 minutes, timing out."
                exit 1
            fi
            i=$((i + 1))
            sleep 5
        done
    done
}

setup_pebble_challtestserv() {
    curl --silent --show-error --data '{"ip":"10.30.50.1"}' http://pebble-challtestsrv:8055/set-default-ipv4
    curl --silent --show-error --data '{"ip":""}' http://pebble-challtestsrv:8055/set-default-ipv6
    curl --silent --show-error --data '{"host":"lim.it", "addresses":["10.0.0.0"]}' http://pebble-challtestsrv:8055/add-a
}

setup_pebble
wait_for_pebble
setup_pebble_challtestserv
docker compose --file "${GITHUB_WORKSPACE}/test/setup/pebble/compose.yaml" logs


================================================
FILE: test/setup/setup-boulder.sh
================================================
#!/bin/bash

set -e

acme_endpoint='http://boulder:4001/directory'

setup_boulder() {
  export GOPATH=${GITHUB_WORKSPACE}/go
  [[ ! -d $GOPATH/src/github.com/letsencrypt/boulder ]] \
    && git clone https://github.com/letsencrypt/boulder \
      "$GOPATH/src/github.com/letsencrypt/boulder"
  pushd "$GOPATH/src/github.com/letsencrypt/boulder"
  git checkout release-2023-12-04
  if [[ "$(uname)" == 'Darwin' ]]; then
    # Set Standard Ports
    for file in test/config/va.json test/config/va-remote-a.json test/config/va-remote-b.json; do
      sed -i '' 's/ 5002/ 80/g' "$file"
      sed -i '' 's/ 5001/ 443/g' "$file"
    done
    # Modify custom rate limit
    sed -i '' 's/le.wtf,le1.wtf/le1.wtf,le2.wtf,le3.wtf/g' test/rate-limit-policies.yml
  else
    # Set Standard Ports
    for file in test/config/va.json test/config/va-remote-a.json test/config/va-remote-b.json; do
      sed --in-place 's/ 5002/ 80/g' "$file"
      sed --in-place 's/ 5001/ 443/g' "$file"
    done
    # Modify custom rate limit
    sed --in-place 's/le.wtf,le1.wtf/le1.wtf,le2.wtf,le3.wtf/g' test/rate-limit-policies.yml
  fi
  docker compose build --pull
  docker compose run -d \
    --use-aliases \
    --name boulder \
    -e FAKE_DNS=10.77.77.1 \
    --service-ports \
    boulder
  popd
}

wait_for_boulder() {
  i=0
  until docker exec boulder bash -c "curl ${acme_endpoint:?} >/dev/null 2>&1"; do
    if [ $i -gt 300 ]; then
      echo "Boulder has not started for 5 minutes, timing out."
      exit 1
    fi
    i=$((i + 5))
    echo "$acme_endpoint : connection refused, Boulder isn't ready yet. Waiting."
    sleep 5
  done
}

setup_boulder
wait_for_boulder


================================================
FILE: test/setup/setup-local.sh
================================================
#!/bin/bash

set -e

function get_environment {
  dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

  LOCAL_BUILD_DIR="$(cd "$dir/../.." && pwd)"
  export GITHUB_WORKSPACE="$LOCAL_BUILD_DIR"

  # shellcheck source=/dev/null
  [[ -f "${GITHUB_WORKSPACE}/test/local_test_env.sh" ]] && \
    source "${GITHUB_WORKSPACE}/test/local_test_env.sh"

  # Get the environment variables from the .github/workflows/test.yml file with sed
  declare -a ci_test_yml
  ci_test_yml[0]="$(sed -n 's/.* NGINX_CONTAINER_NAME: //p' "$LOCAL_BUILD_DIR/.github/workflows/test.yml")"
  ci_test_yml[1]="$(sed -n 's/.* DOCKER_GEN_CONTAINER_NAME: //p' "$LOCAL_BUILD_DIR/.github/workflows/test.yml")"
  ci_test_yml[2]="$(sed -n 's/.* TEST_DOMAINS: //p' "$LOCAL_BUILD_DIR/.github/workflows/test.yml")"

  # If environment variable where sourced or manually set use them, else use those from 
  # .github/workflows/test.yml
  export NGINX_CONTAINER_NAME="${NGINX_CONTAINER_NAME:-${ci_test_yml[0]}}"
  export DOCKER_GEN_CONTAINER_NAME="${DOCKER_GEN_CONTAINER_NAME:-${ci_test_yml[1]}}"
  export TEST_DOMAINS="${TEST_DOMAINS:-${ci_test_yml[2]}}"

  # Build the array containing domains to add to /etc/hosts
  IFS=',' read -r -a domains <<< "$TEST_DOMAINS"

  if [[ -z $SETUP ]]; then
    while true; do
      echo "Which nginx-proxy setup do you want to test or remove ?"
      echo ""
      echo "    1) Two containers setup (nginx-proxy + le-companion)"
      echo "    2) Three containers setup (nginx + docker-gen + le-companion)"
      read -re -p "Select an option [1-2]: " option
      case $option in
        1)
        setup="2containers"
        break
        ;;
        2)
        setup="3containers"
        break
        ;;
        *)
        exit 1
        ;;
      esac
    done
  fi
  export SETUP="${SETUP:-$setup}"

  if [[ -z $ACME_CA ]]; then
    while true; do
      echo "Which ACME CA do you want to use or remove ?"
      echo ""
      echo "    1) Boulder                        https://github.com/letsencrypt/boulder"
      echo "    2) Pebble with base configuration https://github.com/letsencrypt/pebble"
      echo "    3) Pebble with EAB configuration  https://github.com/letsencrypt/pebble"
      read -re -p "Select an option [1-3]: " option
      case $option in
        1)
        acme_ca="boulder"
        break
        ;;
        2)
        acme_ca="pebble"
        pebble_config="pebble-config.json"
        break
        ;;
        3)
        acme_ca="pebble"
        pebble_config="pebble-config-eab.json"
        break
        ;;
        *)
        exit 1
        ;;
      esac
    done
  fi
  export ACME_CA="${ACME_CA:-$acme_ca}"
  export PEBBLE_CONFIG="${PEBBLE_CONFIG:-$pebble_config}"
}

case $1 in
  --setup)
    get_environment

    # Prepare the env file that run.sh will source
    cat > "${GITHUB_WORKSPACE}/test/local_test_env.sh" <<EOF
export GITHUB_WORKSPACE="$LOCAL_BUILD_DIR"
export NGINX_CONTAINER_NAME="$NGINX_CONTAINER_NAME"
export DOCKER_GEN_CONTAINER_NAME="$DOCKER_GEN_CONTAINER_NAME"
export TEST_DOMAINS="$TEST_DOMAINS"
export SETUP="$SETUP"
export ACME_CA="$ACME_CA"
export PEBBLE_CONFIG="$PEBBLE_CONFIG"
EOF

    # Add the required custom entries to /etc/hosts
    echo "Adding custom entries to /etc/hosts (requires sudo)."
    declare -a hosts=("${domains[@]}")
    if [[ "$ACME_CA" == 'pebble' ]]; then
      hosts+=(pebble pebble-challtestsrv)
    fi
    for host in "${hosts[@]}"; do
      grep -q "127.0.0.1 $host # le-companion test suite" /etc/hosts \
        || echo "127.0.0.1 $host # le-companion test suite" \
        | sudo tee -a /etc/hosts
    done

    # Pull nginx:alpine
    docker pull nginx:alpine

    # Prepare the test setup using the setup scripts
    if [[ "$ACME_CA" == 'boulder' ]]; then
      "${GITHUB_WORKSPACE}/test/setup/setup-boulder.sh"
    elif [[ "$ACME_CA" == 'pebble' ]]; then
      "${GITHUB_WORKSPACE}/test/setup/pebble/setup-pebble.sh"
    else
      echo "ACME_CA is not set, aborting."
      exit 1
    fi
    "${GITHUB_WORKSPACE}/test/setup/setup-nginx-proxy.sh"
    ;;

  --teardown)
    get_environment

    # Stop and remove nginx-proxy and (if required) docker-gen
    for cid in $(docker ps -a --filter "label=com.github.nginx-proxy.acme-companion.test-suite" --format "{{.ID}}"); do
      docker stop "$cid"
      docker rm --volumes "$cid"
    done

    if [[ "$ACME_CA" == 'boulder' ]]; then
      # Stop and remove Boulder
      docker stop boulder
      docker compose --project-name 'boulder' \
        --file "${GITHUB_WORKSPACE}/go/src/github.com/letsencrypt/boulder/docker-compose.yml" \
        down --volumes
      docker rm boulder
    elif [[ "$ACME_CA" == 'pebble' ]]; then
      # Stop and remove Pebble
      docker compose --file "${GITHUB_WORKSPACE}/test/setup/pebble/compose.yaml" down
      [[ -f "${GITHUB_WORKSPACE}/pebble.minica.pem" ]] && rm "${GITHUB_WORKSPACE}/pebble.minica.pem"
    fi

    # Cleanup files created by the setup
    if [[ -n "${GITHUB_WORKSPACE// }" ]]; then
      [[ -f "${GITHUB_WORKSPACE}/nginx.tmpl" ]] && rm "${GITHUB_WORKSPACE}/nginx.tmpl"
      rm "${GITHUB_WORKSPACE}/test/local_test_env.sh"
      echo "The ${GITHUB_WORKSPACE}/go folder require superuser permission to fully remove."
      echo "Doing sudo rm -rf in scripts is dangerous, so the folder won't be automatically removed."
    fi

    # Remove custom entries to /etc/hosts
    echo "Removing custom entries from /etc/hosts (requires sudo)."
    declare -a hosts=("${domains[@]}")
    if [[ "$ACME_CA" == 'pebble' ]]; then
      hosts+=(pebble pebble-challtestsrv)
    fi
    for host in "${hosts[@]}"; do
      if [[ "$(uname)" == 'Darwin' ]]; then
        sudo sed -i '' "/127\.0\.0\.1 $host # le-companion test suite/d" /etc/hosts
      else
        sudo sed --in-place "/127\.0\.0\.1 $host # le-companion test suite/d" /etc/hosts
      fi
    done
    ;;

    *)
    echo "Usage:"
    echo ""
    echo "    --setup : setup the test suite."
    echo "    --teardown : remove the test suite containers, configuration and files."
    echo ""
    ;;
esac


================================================
FILE: test/setup/setup-nginx-proxy.sh
================================================
#!/bin/bash

set -e

case $ACME_CA in

  pebble)
    test_net='acme_net'
  ;;

  boulder)
    test_net='boulder_bluenet'
  ;;

  *)
    echo "$0 $ACME_CA: invalid option."
    exit 1

esac

case $SETUP in

  2containers)
    docker run -d -p 80:80 -p 443:443 \
      --name "$NGINX_CONTAINER_NAME" \
      -v /etc/nginx/certs \
      -v /etc/nginx/conf.d \
      -v /etc/nginx/vhost.d \
      -v /usr/share/nginx/html \
      -v /var/run/docker.sock:/tmp/docker.sock:ro \
      --label com.github.nginx-proxy.acme-companion.test-suite \
      --network "$test_net" \
      nginxproxy/nginx-proxy
    ;;

  3containers)
    curl https://raw.githubusercontent.com/nginx-proxy/nginx-proxy/main/nginx.tmpl > "${GITHUB_WORKSPACE}/nginx.tmpl"

    docker run -d -p 80:80 -p 443:443 \
      --name "$NGINX_CONTAINER_NAME" \
      -v /etc/nginx/certs \
      -v /etc/nginx/conf.d \
      -v /etc/nginx/vhost.d \
      -v /usr/share/nginx/html \
      --label com.github.nginx-proxy.acme-companion.test-suite \
      --network "$test_net" \
      nginx:alpine

    docker run -d \
      --name "$DOCKER_GEN_CONTAINER_NAME" \
      --volumes-from "$NGINX_CONTAINER_NAME" \
      -v "${GITHUB_WORKSPACE}/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro" \
      -v /var/run/docker.sock:/tmp/docker.sock:ro \
      --label com.github.nginx-proxy.acme-companion.test-suite \
      --network "$test_net" \
      nginxproxy/docker-gen \
      -notify-sighup "$NGINX_CONTAINER_NAME" -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
    ;;

  *)
    echo "$0 $SETUP: invalid option."
    exit 1

esac


================================================
FILE: test/tests/acme_accounts/expected-std-out.txt
================================================



================================================
FILE: test/tests/acme_accounts/run.sh
================================================
#!/bin/bash

## Test for ACME accounts handling.

if [[ -z $GITHUB_ACTIONS ]]; then
  le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
else
  le_container_name="$(basename "${0%/*}")"
fi
run_le_container "${1:?}" "$le_container_name"

# Create the $domains array from comma separated domains in TEST_DOMAINS.
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"

# Cleanup function with EXIT trap
function cleanup {
  # Remove any remaining Nginx container(s) silently.
  for domain in "${domains[@]}"; do
    docker rm --force "$domain" &> /dev/null
  done
  # Cleanup the files created by this run of the test to avoid foiling following test(s).
  docker exec "$le_container_name" /app/cleanup_test_artifacts
  # Stop the LE container
  docker stop "$le_container_name" > /dev/null
}
trap cleanup EXIT

# Run an nginx container for ${domains[0]}.
run_nginx_container --hosts "${domains[0]}"

# Wait for a symlink at /etc/nginx/certs/${domains[0]}.crt
wait_for_symlink "${domains[0]}" "$le_container_name"

# Hard set the account dir based on the test ACME CA used.
case $ACME_CA in
  pebble)
    account_dir="pebble/dir"
    ;;
  boulder)
    account_dir="boulder/directory"
    ;;
  *)
    echo "$0 $ACME_CA: invalid option."
    exit 1
esac

# Test if the expected folder / file / content are there.
json_file="/etc/acme.sh/default/ca/$account_dir/account.json"
if [[ "$ACME_CA" == 'boulder' ]]; then
  no_mail_str='[]'
elif [[ "$ACME_CA" == 'pebble' ]]; then
  no_mail_str='null'
fi
if docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/default" ]]; then
  echo "The /etc/acme.sh/default folder does not exist."
elif docker exec "$le_container_name" [[ ! -f "$json_file" ]]; then
  echo "The $json_file file does not exist."
elif [[ "$(docker exec "$le_container_name" jq .contact "$json_file")" != "$no_mail_str" ]]; then
  echo "There is an address set on ${json_file}."
  docker exec "$le_container_name" jq . "$json_file"
  docker exec "$le_container_name" jq .contact "$json_file"
fi

# Stop the nginx and companion containers silently.
docker stop "${domains[0]}" &> /dev/null
docker stop "$le_container_name" &> /dev/null

# Run the companion container with the DEFAULT_EMAIL env var set.
default_email="contact@${domains[1]}"
le_container_name="${le_container_name}_default"
run_le_container "${1:?}" "$le_container_name" "--env DEFAULT_EMAIL=${default_email}"

# Run an nginx container for ${domains[1]} without LETSENCRYPT_EMAIL set.
run_nginx_container --hosts "${domains[1]}"

# Wait for a symlink at /etc/nginx/certs/${domains[1]}.crt
wait_for_symlink "${domains[1]}" "$le_container_name"

# Test if the expected folder / file / content are there.
json_file="/etc/acme.sh/${default_email}/ca/$account_dir/account.json"
if docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$default_email" ]]; then
  echo "The /etc/acme.sh/$default_email folder does not exist."
elif docker exec "$le_container_name" [[ ! -f "$json_file" ]]; then
  echo "The $json_file file does not exist."
elif [[ "$(docker exec "$le_container_name" jq -r '.contact|.[0]' "$json_file")" != "mailto:${default_email}" ]]; then
  echo "$default_email is not set on ${json_file}."
  docker exec "$le_container_name" jq . "$json_file"
fi

# Run an nginx container for ${domains[2]} with LETSENCRYPT_EMAIL set.
container_email="contact@${domains[2]}"
run_nginx_container --hosts "${domains[2]}" --cli-args "--env LETSENCRYPT_EMAIL=${container_email}"

# Wait for a symlink at /etc/nginx/certs/${domains[2]}.crt
wait_for_symlink "${domains[2]}" "$le_container_name"

# Test if the expected folder / file / content are there.
json_file="/etc/acme.sh/${container_email}/ca/$account_dir/account.json"
if docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email" ]]; then
  echo "The /etc/acme.sh/$container_email folder does not exist."
elif docker exec "$le_container_name" [[ ! -f "$json_file" ]]; then
  echo "The $json_file file does not exist."
elif [[ "$(docker exec "$le_container_name" jq -r '.contact|.[0]' "$json_file")" != "mailto:${container_email}" ]]; then
  echo "$default_email is not set on ${json_file}."
  docker exec "$le_container_name" jq . "$json_file"
fi

# Stop the nginx containers silently.
docker stop "${domains[1]}" &> /dev/null
docker stop "${domains[2]}" &> /dev/null


================================================
FILE: test/tests/acme_eab/expected-std-out.txt
================================================



================================================
FILE: test/tests/acme_eab/run.sh
================================================
#!/bin/bash

## Test for ACME External Account Binding (EAB).

declare -A eab=( \
  [kid-1]=zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W \
  [kid-2]=b10lLJs8l1GPIzsLP0s6pMt8O0XVGnfTaCeROxQM0BIt2XrJMDHJZBM5NuQmQJQH \
)

if [[ -z $GITHUB_ACTIONS ]]; then
  le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
else
  le_container_name="$(basename "${0%/*}")"
fi
run_le_container "${1:?}" "$le_container_name" \
  --cli-args "--env ACME_EAB_KID=kid-1" \
  --cli-args "--env ACME_EAB_HMAC_KEY=${eab[kid-1]}"

# Create the $domains array from comma separated domains in TEST_DOMAINS.
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"

# Cleanup function with EXIT trap
function cleanup {
  # Remove any remaining Nginx container(s) silently.
  for domain in "${domains[@]}"; do
    docker rm --force "$domain" &> /dev/null
  done
  # Cleanup the files created by this run of the test to avoid foiling following test(s).
  docker exec "$le_container_name" /app/cleanup_test_artifacts
  # Stop the LE container
  docker stop "$le_container_name" > /dev/null
}
trap cleanup EXIT

# Run an nginx container for ${domains[0]}.
run_nginx_container --hosts "${domains[0]}"

# Run an nginx container for ${domains[1]} with LETSENCRYPT_EMAIL and ACME_EAB_* set.
container_email="contact@${domains[1]}"
run_nginx_container --hosts "${domains[1]}"  \
  --cli-args "--env LETSENCRYPT_EMAIL=${container_email}" \
  --cli-args "--env ACME_EAB_KID=kid-2" \
  --cli-args "--env ACME_EAB_HMAC_KEY=${eab[kid-2]}"

# Wait for a symlink at /etc/nginx/certs/${domains[0]}.crt
wait_for_symlink "${domains[0]}" "$le_container_name"

# Test if the expected file is there.
config_path="/etc/acme.sh/default/ca/$ACME_CA/dir"
json_file="${config_path}/account.json"
conf_file="${config_path}/ca.conf"
if docker exec "$le_container_name" [[ ! -f "$json_file" ]]; then
  echo "The $json_file file does not exist."
elif ! docker exec "$le_container_name" grep -q "${eab[kid-1]}" "$conf_file"; then
  echo "There correct EAB HMAC key isn't on ${conf_file}."
fi

# Wait for a symlink at /etc/nginx/certs/${domains[1]}.crt
wait_for_symlink "${domains[1]}" "$le_container_name"

# Test if the expected file is there.
config_path="/etc/acme.sh/${container_email}/ca/$ACME_CA/dir"
json_file="${config_path}/account.json"
conf_file="${config_path}/ca.conf"
if docker exec "$le_container_name" [[ ! -f "$json_file" ]]; then
  echo "The $json_file file does not exist."
elif ! docker exec "$le_container_name" grep -q "${eab[kid-2]}" "$conf_file"; then
  echo "There correct EAB HMAC key isn't on ${conf_file}."
fi

# Stop the nginx containers silently.
docker stop "${domains[0]}" &> /dev/null
docker stop "${domains[1]}" &> /dev/null


================================================
FILE: test/tests/acme_hooks/expected-std-out.txt
================================================



================================================
FILE: test/tests/acme_hooks/run.sh
================================================
#!/bin/bash

## Test for the hooks of acme.sh

default_pre_hook_file="/tmp/default_prehook"
default_pre_hook_command="touch $default_pre_hook_file"
default_post_hook_file="/tmp/default_posthook"
default_post_hook_command="touch $default_post_hook_file"

percontainer_pre_hook_file="/tmp/percontainer_prehook"
percontainer_pre_hook_command="touch $percontainer_pre_hook_file"
percontainer_post_hook_file="/tmp/percontainer_posthook"
percontainer_post_hook_command="touch $percontainer_post_hook_file"

if [[ -z $GITHUB_ACTIONS ]]; then
  le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
else
  le_container_name="$(basename "${0%/*}")"
fi
run_le_container "${1:?}" "$le_container_name" \
  --cli-args "--env ACME_PRE_HOOK=$default_pre_hook_command" \
  --cli-args "--env ACME_POST_HOOK=$default_post_hook_command"

# Create the $domains array from comma separated domains in TEST_DOMAINS.
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"

# Cleanup function with EXIT trap
function cleanup {
  # Remove the Nginx container silently.
  docker rm --force "${domains[0]}" &> /dev/null
  # Cleanup the files created by this run of the test to avoid foiling following test(s).
  docker exec "$le_container_name" /app/cleanup_test_artifacts
  # Stop the LE container
  docker stop "$le_container_name" > /dev/null
}
trap cleanup EXIT

container_email="contact@${domains[0]}"

# Run an nginx container for ${domains[0]} with LETSENCRYPT_EMAIL set.
run_nginx_container --hosts "${domains[0]}" \
  --cli-args "--env LETSENCRYPT_EMAIL=${container_email}"

# Run an nginx container for ${domains[1]} with LETSENCRYPT_EMAIL, ACME_PRE_HOOK and ACME_POST_HOOK set.
run_nginx_container --hosts "${domains[1]}" \
  --cli-args "--env LETSENCRYPT_EMAIL=${container_email}" \
  --cli-args "--env ACME_PRE_HOOK=$percontainer_pre_hook_command" \
  --cli-args "--env ACME_POST_HOOK=$percontainer_post_hook_command"

# Wait for a symlink at /etc/nginx/certs/${domains[0]}.crt
wait_for_symlink "${domains[0]}" "$le_container_name"

acme_pre_hook_key="Le_PreHook="
acme_post_hook_key="Le_PostHook="
acme_base64_start="'__ACME_BASE64__START_"
acme_base64_end="__ACME_BASE64__END_'"

# Check if the default command is deliverd properly in /etc/acme.sh
if docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email" ]]; then
  echo "The /etc/acme.sh/$container_email folder does not exist."
elif docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email/${domains[0]}" ]]; then
  echo "The /etc/acme.sh/$container_email/${domains[0]} folder does not exist."
elif docker exec "$le_container_name" [[ ! -f "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf" ]]; then
  echo "The /etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf file does not exist."
fi

default_pre_hook_command_base64="${acme_pre_hook_key}${acme_base64_start}$(echo -n "$default_pre_hook_command" | base64)${acme_base64_end}"
default_post_hook_command_base64="${acme_post_hook_key}${acme_base64_start}$(echo -n "$default_post_hook_command" | base64)${acme_base64_end}"

default_acme_pre_hook="$(docker exec "$le_container_name" grep "$acme_pre_hook_key" "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf")"
default_acme_post_hook="$(docker exec "$le_container_name" grep "$acme_post_hook_key" "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf")"

if [[ "$default_pre_hook_command_base64" != "$default_acme_pre_hook" ]]; then 
  echo "Default prehook command not saved properly"
fi
if [[ "$default_post_hook_command_base64" != "$default_acme_post_hook" ]]; then 
  echo "Default posthook command not saved properly"
fi


# Check if the default action is performed 
if docker exec "$le_container_name" [[ ! -f "$default_pre_hook_file" ]]; then
  echo "Default prehook action failed"
fi
if docker exec "$le_container_name" [[ ! -f "$default_post_hook_file" ]]; then
  echo "Default posthook action failed"
fi

# Wait for a symlink at /etc/nginx/certs/${domains[1]}.crt
wait_for_symlink "${domains[1]}" "$le_container_name"

# Check if the per-container command is deliverd properly in /etc/acme.sh
if docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email/${domains[1]}" ]]; then
  echo "The /etc/acme.sh/$container_email/${domains[1]} folder does not exist."
elif docker exec "$le_container_name" [[ ! -f "/etc/acme.sh/$container_email/${domains[1]}/${domains[1]}.conf" ]]; then
  echo "The /etc/acme.sh/$container_email/${domains[1]}/${domains[1]}.conf file does not exist."
fi

percontainer_pre_hook_command_base64="${acme_pre_hook_key}${acme_base64_start}$(echo -n "$percontainer_pre_hook_command" | base64)${acme_base64_end}"
percontainer_post_hook_command_base64="${acme_post_hook_key}${acme_base64_start}$(echo -n "$percontainer_post_hook_command" | base64)${acme_base64_end}"

percontainer_acme_pre_hook="$(docker exec "$le_container_name" grep "$acme_pre_hook_key" "/etc/acme.sh/$container_email/${domains[1]}/${domains[1]}.conf")"
percontainer_acme_post_hook="$(docker exec "$le_container_name" grep "$acme_post_hook_key" "/etc/acme.sh/$container_email/${domains[1]}/${domains[1]}.conf")"

if [[ "$percontainer_pre_hook_command_base64" != "$percontainer_acme_pre_hook" ]]; then 
  echo "Per-container prehook command not saved properly"
fi
if [[ "$percontainer_post_hook_command_base64" != "$percontainer_acme_post_hook" ]]; then 
  echo "Per-container posthook command not saved properly"
fi


# Check if the percontainer action is performed 
if docker exec "$le_container_name" [[ ! -f "$percontainer_pre_hook_file" ]]; then
  echo "Per-container prehook action failed"
fi
if docker exec "$le_container_name" [[ ! -f "$percontainer_post_hook_file" ]]; then
  echo "Per-container posthook action failed"
fi


================================================
FILE: test/tests/certs_default_renew/expected-std-out.txt
================================================



================================================
FILE: test/tests/certs_default_renew/run.sh
================================================
#!/bin/bash

## Test for the DEFAULT_RENEW function.

if [[ -z $GITHUB_ACTIONS ]]; then
  le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
else
  le_container_name="$(basename "${0%/*}")"
fi

default_renew=170
run_le_container "${1:?}" "$le_container_name" \
  --cli-args "--env DEFAULT_RENEW=$default_renew"

# Create the $domains array from comma separated domains in TEST_DOMAINS.
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"

# Cleanup function with EXIT trap
function cleanup {
  # Remove the Nginx container silently.
  docker rm --force "${domains[0]}" &> /dev/null
  # Cleanup the files created by this run of the test to avoid foiling following test(s).
  docker exec "$le_container_name" /app/cleanup_test_artifacts
  # Stop the LE container
  docker stop "$le_container_name" > /dev/null
}
trap cleanup EXIT

container_email="contact@${domains[0]}"
acme_config_file="/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf"

# Run a nginx container for ${domains[0]} with LETSENCRYPT_EMAIL set.
run_nginx_container --hosts "${domains[0]}" \
  --cli-args "--env LETSENCRYPT_EMAIL=${container_email}"

# Wait for a symlink at /etc/nginx/certs/${domains[0]}.crt
wait_for_symlink "${domains[0]}" "$le_container_name"

acme_cert_create_time_key="Le_CertCreateTime="
acme_renewal_days_key="Le_RenewalDays="
acme_next_renew_time_key="Le_NextRenewTime="

# Check if the default command is deliverd properly in /etc/acme.sh
if docker exec "$le_container_name" [[ ! -f "$acme_config_file" ]]; then
  echo "The $acme_config_file file does not exist."
fi

cert_create_time="$(docker exec "$le_container_name" grep "$acme_cert_create_time_key" "$acme_config_file" | cut -f2 -d\')"
expected_renewal_days="${acme_renewal_days_key}'$default_renew'"
expected_next_renew_time="${acme_next_renew_time_key}'$(($cert_create_time + $default_renew * 24 * 60 * 60 - 86400))'"
actual_renewal_days="$(docker exec "$le_container_name" grep "$acme_renewal_days_key" "$acme_config_file")"
actual_next_renew_time="$(docker exec "$le_container_name" grep "$acme_next_renew_time_key" "$acme_config_file")"

if [[ "$expected_renewal_days" != "$actual_renewal_days" ]]; then
  echo "Renewal days is not correct"
fi
if [[ "$expected_next_renew_time" != "$actual_next_renew_time" ]]; then
  echo "Next renewal time is not correct"
fi


================================================
FILE: test/tests/certs_san/expected-std-out.txt
================================================



================================================
FILE: test/tests/certs_san/run.sh
================================================
#!/bin/bash

## Test for SAN (Subject Alternative Names) certificates.

if [[ -z $GITHUB_ACTIONS ]]; then
  le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
else
  le_container_name="$(basename "${0%/*}")"
fi
run_le_container "${1:?}" "$le_container_name"

# Create the $domains array from comma separated domains in TEST_DOMAINS.
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"

# Cleanup function with EXIT trap
function cleanup {
  # Remove any remaining Nginx container(s) silently.
  i=1
  for hosts in "${letsencrypt_hosts[@]}"; do
    docker rm --force "test$i" &> /dev/null
    i=$(( i + 1 ))
  done
  # Cleanup the files created by this run of the test to avoid foiling following test(s).
  docker exec "$le_container_name" /app/cleanup_test_artifacts
  # Stop the LE container
  docker stop "$le_container_name" > /dev/null
}
trap cleanup EXIT

# Create three different comma separated list from the first three domains in $domains.
# testing for regression on spaced lists https://github.com/nginx-proxy/acme-companion/issues/288
# with trailing comma https://github.com/nginx-proxy/acme-companion/issues/254
# and with trailing dot https://github.com/nginx-proxy/acme-companion/issues/676
letsencrypt_hosts=( \
  [0]="${domains[0]},${domains[1]},${domains[2]}" \     #straight comma separated list
  [1]="${domains[1]}, ${domains[2]}, ${domains[0]}" \   #comma separated list with spaces
  [2]="${domains[2]}, ${domains[0]}, ${domains[1]}," \  #comma separated list with spaces and a trailing comma
  [3]="${domains[0]}.,${domains[2]}.,${domains[1]}" )   #trailing dots

i=1

for hosts in "${letsencrypt_hosts[@]}"; do

  # Get the base domain (first domain of the list).
  base_domain="$(get_base_domain "$hosts")"
  container="test$i"

  # Run an Nginx container passing one of the comma separated list as LETSENCRYPT_HOST env var.
  run_nginx_container --hosts "$hosts" --name "$container"

  # Wait for a symlink at /etc/nginx/certs/$base_domain.crt
  if wait_for_symlink "$base_domain" "$le_container_name" "./${base_domain}/fullchain.pem"; then
    # then grab the certificate in text form ...
    created_cert="$(docker exec "$le_container_name" \
      openssl x509 -in "/etc/nginx/certs/${base_domain}/cert.pem" -text -noout)"
    # ... as well as the certificate fingerprint.
    created_cert_fingerprint="$(docker exec "$le_container_name" \
      openssl x509 -in "/etc/nginx/certs/${base_domain}/cert.pem" -fingerprint -noout)"
  fi

  for domain in "${domains[@]}"; do
  ## For all the domains in the $domains array ...

    # Check if the domain is on the certificate.
    if ! grep -q "$domain" <<< "$created_cert"; then
      echo "$domain did not appear on certificate."
    elif [[ "${DRY_RUN:-}" == 1 ]]; then
      echo "$domain is on certificate."
    fi

    # Wait for a connection to https://domain then grab the served certificate in text form.
    wait_for_conn --domain "$domain"
    served_cert_fingerprint="$(echo \
      | openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
      | openssl x509 -fingerprint -noout)"


    # Compare the cert on file and what we got from the https connection.
    # If not identical, display a full diff.
    if [ "$created_cert_fingerprint" != "$served_cert_fingerprint" ]; then
      echo "Nginx served an incorrect certificate for $domain."
      served_cert="$(echo \
        | openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
        | openssl x509 -text -noout \
        | sed 's/ = /=/g' )"
      diff -u <(echo "${created_cert// = /=}") <(echo "$served_cert")
    elif [[ "${DRY_RUN:-}" == 1 ]]; then
       echo "The correct certificate for $domain was served by Nginx."
    fi
  done

  docker stop "$container" &> /dev/null
  docker exec "$le_container_name" /app/cleanup_test_artifacts
  i=$(( i + 1 ))

done


================================================
FILE: test/tests/certs_single/expected-std-out.txt
================================================



================================================
FILE: test/tests/certs_single/run.sh
================================================
#!/bin/bash

## Test for single domain certificates.

if [[ -z $GITHUB_ACTIONS ]]; then
  le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
else
  le_container_name="$(basename "${0%/*}")"
fi
run_le_container "${1:?}" "$le_container_name"

# Create the $domains array from comma separated domains in TEST_DOMAINS.
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"

# Cleanup function with EXIT trap
function cleanup {
  # Remove any remaining Nginx container(s) silently.
  for domain in "${domains[@]}"; do
    docker rm --force "$domain" &> /dev/null
  done
  # Cleanup the files created by this run of the test to avoid foiling following test(s).
  docker exec "$le_container_name" /app/cleanup_test_artifacts
  # Stop the LE container
  docker stop "$le_container_name" > /dev/null
}
trap cleanup EXIT

# Run a separate nginx container for each domain in the $domains array.
# Start all the containers in a row so that docker-gen debounce timers fire only once.
for domain in "${domains[@]}"; do
  run_nginx_container --hosts "$domain"
done

for domain in "${domains[@]}"; do

  # Wait for a symlink at /etc/nginx/certs/$domain.crt
  if wait_for_symlink "$domain" "$le_container_name" "./${domain}/fullchain.pem" ; then
    # then grab the certificate in text form from the file ...
    created_cert="$(docker exec "$le_container_name" \
      openssl x509 -in "/etc/nginx/certs/${domain}/cert.pem" -text -noout)"
    # ... as well as the certificate fingerprint.
    created_cert_fingerprint="$(docker exec "$le_container_name" \
      openssl x509 -in "/etc/nginx/certs/${domain}/cert.pem" -fingerprint -noout)"
  fi


  # Check if the domain is on the certificate.
  if ! grep -q "$domain" <<< "$created_cert"; then
    echo "Domain $domain isn't on certificate."
  elif [[ "${DRY_RUN:-}" == 1 ]]; then
    echo "Domain $domain is on certificate."
  fi

  # Wait for a connection to https://domain then grab the served certificate fingerprint.
  wait_for_conn --domain "$domain"
  served_cert_fingerprint="$(echo \
    | openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
    | openssl x509 -fingerprint -noout)"

  # Compare fingerprints from the cert on file and what we got from the https connection.
  # If not identical, display a full diff.
  if [ "$created_cert_fingerprint" != "$served_cert_fingerprint" ]; then
    echo "Nginx served an incorrect certificate for $domain."
    served_cert="$(echo \
      | openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
      | openssl x509 -text -noout \
      | sed 's/ = /=/g' )"
    diff -u <(echo "${created_cert// = /=}") <(echo "$served_cert")
  elif [[ "${DRY_RUN:-}" == 1 ]]; then
    echo "The correct certificate for $domain was served by Nginx."
  fi

  # Stop the Nginx container silently.
  docker stop "$domain" > /dev/null
done


================================================
FILE: test/tests/certs_single_domain/expected-std-out.txt
================================================



================================================
FILE: test/tests/certs_single_domain/run.sh
================================================
#!/bin/bash

## Test for spliting SAN certificates into single domain certificates by NGINX container env variables

if [[ -z $GITHUB_ACTIONS ]]; then
  le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
else
  le_container_name="$(basename "${0%/*}")"
fi
run_le_container "${1:?}" "$le_container_name"

# Create the $domains array from comma separated domains in TEST_DOMAINS.
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"

# Cleanup function with EXIT trap
function cleanup {
  # Remove any remaining Nginx container(s) silently.
  i=1
  for hosts in "${letsencrypt_hosts[@]}"; do
    docker rm --force "test$i" &> /dev/null
    i=$(( i + 1 ))
  done
  # Cleanup the files created by this run of the test to avoid foiling following test(s).
  docker exec "$le_container_name" /app/cleanup_test_artifacts
  # Stop the LE container
  docker stop "$le_container_name" > /dev/null
}
trap cleanup EXIT

# Create three different comma separated list from the first three domains in $domains.
# testing for regression on spaced lists https://github.com/nginx-proxy/acme-companion/issues/288
# and w
Download .txt
gitextract_zsr4to63/

├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── bug_report.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── build-publish.yml
│       ├── dockerhub-description.yml
│       └── test.yml
├── .gitignore
├── .shellcheckrc
├── Dockerfile
├── LICENSE
├── README.md
├── app/
│   ├── cert_status
│   ├── cleanup_test_artifacts
│   ├── dhparam/
│   │   ├── ffdhe2048.pem
│   │   ├── ffdhe3072.pem
│   │   └── ffdhe4096.pem
│   ├── dhparam.pem.default
│   ├── entrypoint.sh
│   ├── force_renew
│   ├── functions.sh
│   ├── letsencrypt_service
│   ├── letsencrypt_service_data.tmpl
│   ├── nginx_location.conf
│   ├── signal_le_service
│   └── start.sh
├── docs/
│   ├── Advanced-usage.md
│   ├── Basic-usage.md
│   ├── Container-configuration.md
│   ├── Container-utilities.md
│   ├── Docker-Compose.md
│   ├── Getting-containers-IDs.md
│   ├── Google-Trust-Services.md
│   ├── Hooks.md
│   ├── Invalid-authorizations.md
│   ├── Let's-Encrypt-and-ACME.md
│   ├── Persistent-data.md
│   ├── README.md
│   ├── Standalone-certificates.md
│   └── Zero-SSL.md
├── install_acme.sh
└── test/
    ├── README.md
    ├── config.sh
    ├── github_actions/
    │   └── containers-logs.sh
    ├── run.sh
    ├── setup/
    │   ├── pebble/
    │   │   ├── compose.yaml
    │   │   ├── pebble-config-eab.json
    │   │   ├── pebble-config.json
    │   │   └── setup-pebble.sh
    │   ├── setup-boulder.sh
    │   ├── setup-local.sh
    │   └── setup-nginx-proxy.sh
    └── tests/
        ├── acme_accounts/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── acme_eab/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── acme_hooks/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── certs_default_renew/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── certs_san/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── certs_single/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── certs_single_domain/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── certs_standalone/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── container_restart/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── docker_api/
        │   └── run.sh
        ├── docker_api_legacy/
        │   └── run.sh
        ├── force_renew/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── location_config/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── ocsp_must_staple/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── permissions_custom/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── permissions_default/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── private_keys/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── symlinks/
        │   ├── expected-std-out.txt
        │   └── run.sh
        ├── test-functions.sh
        └── unit_tests/
            └── expected-std-out.txt
Condensed preview — 87 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (271K chars).
[
  {
    "path": ".dockerignore",
    "chars": 43,
    "preview": "+.*\n+docs\n+go\n+test\n+README.md\n+schema.png\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 2126,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\nIf your are usin"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 417,
    "preview": "version: 2\nupdates:\n  # Maintain dependencies for Docker\n  - package-ecosystem: \"docker\"\n    directory: \"/\"\n    schedule"
  },
  {
    "path": ".github/workflows/build-publish.yml",
    "chars": 2699,
    "preview": "name: Build and publish Docker image\n\npermissions:\n  contents: read\n  packages: write\n\non:\n  workflow_dispatch:\n  schedu"
  },
  {
    "path": ".github/workflows/dockerhub-description.yml",
    "chars": 736,
    "preview": "name: Update Docker Hub Description\n\npermissions:\n  contents: read\n\non:\n  push:\n    branches:\n      - main\n    paths:\n  "
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 3982,
    "preview": "name: Tests\n\npermissions:\n  contents: read\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n    paths-ignore"
  },
  {
    "path": ".gitignore",
    "chars": 289,
    "preview": ".docker\n.vscode\ncerts/\nconf.d/\ndata/\nvhost.d/\n\n# tests\ngo/\nnginx.tmpl\npebble.minica.pem\ntest/local_test_env.sh\ntest/test"
  },
  {
    "path": ".shellcheckrc",
    "chars": 22,
    "preview": "external-sources=true\n"
  },
  {
    "path": "Dockerfile",
    "chars": 1022,
    "preview": "# syntax=docker/dockerfile:1\nFROM docker.io/nginxproxy/docker-gen:0.16.2 AS docker-gen\n\nFROM docker.io/library/alpine:3."
  },
  {
    "path": "LICENSE",
    "chars": 1124,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2015-2017 Yves Blusseau\nCopyright (c) 2017-2022 Nicolas Duchon\n\nPermission is hereb"
  },
  {
    "path": "README.md",
    "chars": 7381,
    "preview": "[![Tests](https://github.com/nginx-proxy/acme-companion/actions/workflows/test.yml/badge.svg)](https://github.com/nginx-"
  },
  {
    "path": "app/cert_status",
    "chars": 2650,
    "preview": "#!/bin/bash\nfunction print_cert_info {\n  local enddate\n  local subject\n  local san_str\n\n  # Get the wanted informations "
  },
  {
    "path": "app/cleanup_test_artifacts",
    "chars": 1122,
    "preview": "#!/bin/bash\n\n# This script should not be run outside of a test container\n[[ \"$TEST_MODE\" == 'true' ]] || exit 1\n\nwhile ["
  },
  {
    "path": "app/dhparam/ffdhe2048.pem",
    "chars": 424,
    "preview": "-----BEGIN DH PARAMETERS-----\nMIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz\n+8yTnc4kmz75fS/jY2MMddj2g"
  },
  {
    "path": "app/dhparam/ffdhe3072.pem",
    "chars": 595,
    "preview": "-----BEGIN DH PARAMETERS-----\nMIIBiAKCAYEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz\n+8yTnc4kmz75fS/jY2MMddj2g"
  },
  {
    "path": "app/dhparam/ffdhe4096.pem",
    "chars": 769,
    "preview": "-----BEGIN DH PARAMETERS-----\nMIICCAKCAgEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz\n+8yTnc4kmz75fS/jY2MMddj2g"
  },
  {
    "path": "app/dhparam.pem.default",
    "chars": 424,
    "preview": "-----BEGIN DH PARAMETERS-----\nMIIBCAKCAQEAwpR+yYapElMV4DiO+BwKK2N8Ur4giZtga+dslyDMuhY+U4t/97Eq\ngdFg2RD5nqrgWCRWEYcbh1kPB"
  },
  {
    "path": "app/entrypoint.sh",
    "chars": 9273,
    "preview": "#!/bin/bash\n\nset -u\n\n# shellcheck source=functions.sh\nsource /app/functions.sh\n\nfunction print_version {\n    if [[ -n \"$"
  },
  {
    "path": "app/force_renew",
    "chars": 127,
    "preview": "#!/bin/bash\n\n# shellcheck source=letsencrypt_service\nsource /app/letsencrypt_service --source-only\n\nupdate_certs --force"
  },
  {
    "path": "app/functions.sh",
    "chars": 16131,
    "preview": "#!/bin/bash\n\n# Convert argument to lowercase (bash 4 only)\nfunction lc {\n\techo \"${@,,}\"\n}\n\nDEBUG=\"$(lc \"${DEBUG:-}\")\"\nif"
  },
  {
    "path": "app/letsencrypt_service",
    "chars": 23515,
    "preview": "#!/bin/bash\n\n# shellcheck source=functions.sh\nsource /app/functions.sh\n\nCERTS_UPDATE_INTERVAL=\"${CERTS_UPDATE_INTERVAL:-"
  },
  {
    "path": "app/letsencrypt_service_data.tmpl",
    "chars": 6512,
    "preview": "#!/bin/bash\n# shellcheck disable=SC2034\n{{- $DEFAULT_ACMESH_DNS_API_CONFIG := fromYaml (coalesce $.Env.ACMESH_DNS_API_CO"
  },
  {
    "path": "app/nginx_location.conf",
    "chars": 170,
    "preview": "location ^~ /.well-known/acme-challenge/ {\n    auth_basic off;\n    auth_request off;\n    allow all;\n    root /usr/share/"
  },
  {
    "path": "app/signal_le_service",
    "chars": 75,
    "preview": "#!/bin/bash\n\n# Using busybox pkill\npkill -USR1 -f /app/letsencrypt_service\n"
  },
  {
    "path": "app/start.sh",
    "chars": 858,
    "preview": "#!/bin/bash\n\n# SIGTERM-handler\nterm_handler() {\n    [[ -n \"$docker_gen_pid\" ]] && kill \"$docker_gen_pid\"\n    [[ -n \"$let"
  },
  {
    "path": "docs/Advanced-usage.md",
    "chars": 3477,
    "preview": "## Advanced usage (with the nginx and docker-gen containers)\n\n**nginx-proxy** can also be run as two separate containers"
  },
  {
    "path": "docs/Basic-usage.md",
    "chars": 3657,
    "preview": "## Basic usage (with the nginx-proxy container)\n\nTwo writable volumes must be declared on the **nginx-proxy** container "
  },
  {
    "path": "docs/Container-configuration.md",
    "chars": 5489,
    "preview": "## Optional container environment variables for custom configuration.\n\n* `ACME_CA_URI` - Directory URI for the CA ACME A"
  },
  {
    "path": "docs/Container-utilities.md",
    "chars": 870,
    "preview": "The container provide the following utilities (replace `nginx-proxy-acme` with the name or ID of your **acme-companion**"
  },
  {
    "path": "docs/Docker-Compose.md",
    "chars": 4404,
    "preview": "## Usage with Docker Compose\n\nAs stated by its repository, [Docker Compose](https://github.com/docker/compose) is a tool"
  },
  {
    "path": "docs/Getting-containers-IDs.md",
    "chars": 4085,
    "preview": "## Getting nginx-proxy/nginx/docker-gen containers IDs\n\nFor **acme-companion** to work properly, it needs to know the ID"
  },
  {
    "path": "docs/Google-Trust-Services.md",
    "chars": 969,
    "preview": "## Google Trust Services\n\n[Google Trust Service](https://pki.goog/) is an ACME CA with generous default quota and high u"
  },
  {
    "path": "docs/Hooks.md",
    "chars": 3678,
    "preview": "## Pre-Hooks and Post-Hooks\n\nThe Pre- and Post-Hooks of [acme.sh](https://github.com/acmesh-official/acme.sh/) are avail"
  },
  {
    "path": "docs/Invalid-authorizations.md",
    "chars": 3534,
    "preview": "## Troubleshooting failing authorizations\n\nThe first two things to do in case of failing authorization are to run the **"
  },
  {
    "path": "docs/Let's-Encrypt-and-ACME.md",
    "chars": 10838,
    "preview": "## Let's Encrypt / ACME\n\n**NOTE on CAA**: Please ensure that your DNS provider answers correctly to CAA record requests."
  },
  {
    "path": "docs/Persistent-data.md",
    "chars": 4879,
    "preview": "## Persistent data\n\n### Named volumes (recommended)\n\nWhen you follow instructions from Basic usage or Advanced usage, Do"
  },
  {
    "path": "docs/README.md",
    "chars": 706,
    "preview": "#### Usage:\n\n[Basic (two containers).](./Basic-usage.md)\n\n[Advanced (three containers).](./Advanced-usage.md)\n\n[Getting "
  },
  {
    "path": "docs/Standalone-certificates.md",
    "chars": 5130,
    "preview": "## Standalone certificates\n\nYou can generate certificate that are not tied to containers environment variable by mountin"
  },
  {
    "path": "docs/Zero-SSL.md",
    "chars": 1402,
    "preview": "## Zero SSL\n\n[Zero SSL](https://zerossl.com/) is an ACME CA that offer some advantages over Let's Encrypt:\n- no staging "
  },
  {
    "path": "install_acme.sh",
    "chars": 542,
    "preview": "#!/bin/bash\n\nset -e\n\n# Install git (required to fetch acme.sh)\napk --no-cache --virtual .acmesh-deps add git\n\n# Get acme"
  },
  {
    "path": "test/README.md",
    "chars": 905,
    "preview": "### acme-companion test suite\n\nThe test suite can be run locally on a Linux or macOS host.\n\nTo prepare the test setup:\n\n"
  },
  {
    "path": "test/config.sh",
    "chars": 498,
    "preview": "#!/bin/bash\nset -e\n\nglobalTests+=(\n\tdocker_api\n\tdocker_api_legacy\n\tlocation_config\n\tcerts_single\n\tcerts_san\n\tcerts_singl"
  },
  {
    "path": "test/github_actions/containers-logs.sh",
    "chars": 809,
    "preview": "#!/bin/bash\n\nbold_echo() {\n  echo -e \"\\033[33;1m$1\\033[0m\"\n}\n\nif [[ -f \"$GITHUB_WORKSPACE/test/github_actions/failed_tes"
  },
  {
    "path": "test/run.sh",
    "chars": 17367,
    "preview": "#!/usr/bin/env bash\n#shellcheck disable=SC2068,SC2206\n\n# This file is adapted from the Docker official images test suite"
  },
  {
    "path": "test/setup/pebble/compose.yaml",
    "chars": 886,
    "preview": "services:\n  pebble:\n    image: \"ghcr.io/letsencrypt/pebble:${PEBBLE_VERSION}\"\n    container_name: pebble\n    volumes:\n  "
  },
  {
    "path": "test/setup/pebble/pebble-config-eab.json",
    "chars": 631,
    "preview": "{\n  \"pebble\": {\n    \"listenAddress\": \"0.0.0.0:14000\",\n    \"managementListenAddress\": \"0.0.0.0:15000\",\n    \"certificate\":"
  },
  {
    "path": "test/setup/pebble/pebble-config.json",
    "chars": 428,
    "preview": "{\n  \"pebble\": {\n    \"listenAddress\": \"0.0.0.0:14000\",\n    \"managementListenAddress\": \"0.0.0.0:15000\",\n    \"certificate\":"
  },
  {
    "path": "test/setup/pebble/setup-pebble.sh",
    "chars": 1300,
    "preview": "#!/bin/bash\n\nset -e\n\nsetup_pebble() {\n    curl --silent --show-error https://raw.githubusercontent.com/letsencrypt/pebbl"
  },
  {
    "path": "test/setup/setup-boulder.sh",
    "chars": 1653,
    "preview": "#!/bin/bash\n\nset -e\n\nacme_endpoint='http://boulder:4001/directory'\n\nsetup_boulder() {\n  export GOPATH=${GITHUB_WORKSPACE"
  },
  {
    "path": "test/setup/setup-local.sh",
    "chars": 6054,
    "preview": "#!/bin/bash\n\nset -e\n\nfunction get_environment {\n  dir=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\n  LOCAL_BUILD"
  },
  {
    "path": "test/setup/setup-nginx-proxy.sh",
    "chars": 1612,
    "preview": "#!/bin/bash\n\nset -e\n\ncase $ACME_CA in\n\n  pebble)\n    test_net='acme_net'\n  ;;\n\n  boulder)\n    test_net='boulder_bluenet'"
  },
  {
    "path": "test/tests/acme_accounts/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/tests/acme_accounts/run.sh",
    "chars": 4316,
    "preview": "#!/bin/bash\n\n## Test for ACME accounts handling.\n\nif [[ -z $GITHUB_ACTIONS ]]; then\n  le_container_name=\"$(basename \"${0"
  },
  {
    "path": "test/tests/acme_eab/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/tests/acme_eab/run.sh",
    "chars": 2726,
    "preview": "#!/bin/bash\n\n## Test for ACME External Account Binding (EAB).\n\ndeclare -A eab=( \\\n  [kid-1]=zWNDZM6eQGHWpSRTPal5eIUYFTu7"
  },
  {
    "path": "test/tests/acme_hooks/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/tests/acme_hooks/run.sh",
    "chars": 5771,
    "preview": "#!/bin/bash\n\n## Test for the hooks of acme.sh\n\ndefault_pre_hook_file=\"/tmp/default_prehook\"\ndefault_pre_hook_command=\"to"
  },
  {
    "path": "test/tests/certs_default_renew/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/tests/certs_default_renew/run.sh",
    "chars": 2340,
    "preview": "#!/bin/bash\n\n## Test for the DEFAULT_RENEW function.\n\nif [[ -z $GITHUB_ACTIONS ]]; then\n  le_container_name=\"$(basename "
  },
  {
    "path": "test/tests/certs_san/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/tests/certs_san/run.sh",
    "chars": 3892,
    "preview": "#!/bin/bash\n\n## Test for SAN (Subject Alternative Names) certificates.\n\nif [[ -z $GITHUB_ACTIONS ]]; then\n  le_container"
  },
  {
    "path": "test/tests/certs_single/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/tests/certs_single/run.sh",
    "chars": 2888,
    "preview": "#!/bin/bash\n\n## Test for single domain certificates.\n\nif [[ -z $GITHUB_ACTIONS ]]; then\n  le_container_name=\"$(basename "
  },
  {
    "path": "test/tests/certs_single_domain/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/tests/certs_single_domain/run.sh",
    "chars": 4127,
    "preview": "#!/bin/bash\n\n## Test for spliting SAN certificates into single domain certificates by NGINX container env variables\n\nif "
  },
  {
    "path": "test/tests/certs_standalone/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/tests/certs_standalone/run.sh",
    "chars": 4031,
    "preview": "#!/bin/bash\n\n## Test for standalone certificates.\n\ncase $ACME_CA in\n  pebble)\n    test_net='acme_net'\n  ;;\n  boulder)\n  "
  },
  {
    "path": "test/tests/container_restart/expected-std-out.txt",
    "chars": 84,
    "preview": "Container le1.wtf restarted\nContainer le2.wtf restarted\nContainer le3.wtf restarted\n"
  },
  {
    "path": "test/tests/container_restart/run.sh",
    "chars": 1829,
    "preview": "#!/bin/bash\n\n## Test for LETSENCRYPT_RESTART_CONTAINER variable.\n\nif [[ -z $GITHUB_ACTIONS ]]; then\n  le_container_name="
  },
  {
    "path": "test/tests/docker_api/run.sh",
    "chars": 9053,
    "preview": "#!/bin/bash\n\n## Test for the Docker API.\n\nnginx_vol='nginx-volumes-from'\nnginx_env='nginx-env-var'\nnginx_lbl='nginx-labe"
  },
  {
    "path": "test/tests/docker_api_legacy/run.sh",
    "chars": 9247,
    "preview": "#!/bin/bash\n\n## Test for the Docker API with legacy labels.\n\nnginx_vol='nginx-volumes-from-legacy'\nnginx_env='nginx-env-"
  },
  {
    "path": "test/tests/force_renew/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/tests/force_renew/run.sh",
    "chars": 1727,
    "preview": "#!/bin/bash\n\n## Test for the /app/force_renew script.\n\nif [[ -z $GITHUB_ACTIONS ]]; then\n  le_container_name=\"$(basename"
  },
  {
    "path": "test/tests/location_config/expected-std-out.txt",
    "chars": 154,
    "preview": "*.bar.baz.com.example.com\n*.baz.com.example.com\n*.com.example.com\n*.example.com\nfoo.bar.baz.com.example.*\nfoo.bar.baz.co"
  },
  {
    "path": "test/tests/location_config/run.sh",
    "chars": 8171,
    "preview": "#!/bin/bash\n\n## Test for automatic location configuration.\n\n# Set variables\ntest_comment='### This is a test comment'\nvh"
  },
  {
    "path": "test/tests/ocsp_must_staple/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/tests/ocsp_must_staple/run.sh",
    "chars": 2222,
    "preview": "#!/bin/bash\n\n## Test for OCSP Must-Staple extension.\n\nif [[ -z $GITHUB_ACTIONS ]]; then\n  le_container_name=\"$(basename "
  },
  {
    "path": "test/tests/permissions_custom/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/tests/permissions_custom/run.sh",
    "chars": 3335,
    "preview": "#!/bin/bash\n\n## Test for sensitive files and folders permissions\n\nfiles_uid=1000\nfiles_gid=1001\nfiles_perms=644\nfolders_"
  },
  {
    "path": "test/tests/permissions_default/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/tests/permissions_default/run.sh",
    "chars": 2920,
    "preview": "#!/bin/bash\n\n## Test for sensitive files and folders permissions\n\nif [[ -z $GITHUB_ACTIONS ]]; then\n  le_container_name="
  },
  {
    "path": "test/tests/private_keys/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "test/tests/private_keys/run.sh",
    "chars": 1851,
    "preview": "#!/usr/bin/env bash\n\n## Test for private keys types\n\nif [[ -z $GITHUB_ACTIONS ]]; then\n  le_container_name=\"$(basename \""
  },
  {
    "path": "test/tests/symlinks/expected-std-out.txt",
    "chars": 79,
    "preview": "Symlink for lim.it certificate was not generated under one minute, timing out.\n"
  },
  {
    "path": "test/tests/symlinks/run.sh",
    "chars": 5122,
    "preview": "#!/bin/bash\n\n## Test for symlink creation / removal.\n\nif [[ -z $GITHUB_ACTIONS ]]; then\n  le_container_name=\"$(basename "
  },
  {
    "path": "test/tests/test-functions.sh",
    "chars": 8885,
    "preview": "#!/bin/bash\n\nset -e\n\n# Get the first domain of a comma separated list.\nfunction get_base_domain {\n  awk -F ',' '{print $"
  },
  {
    "path": "test/tests/unit_tests/expected-std-out.txt",
    "chars": 1,
    "preview": "\n"
  }
]

About this extraction

This page contains the full source code of the nginx-proxy/acme-companion GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 87 files (247.1 KB), approximately 71.0k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!