├── docker-host-ipc ├── Dockerfile ├── namedpipe └── README.md ├── auto-letsencrypt-cloudflare ├── Dockerfile ├── scripts │ ├── cron │ └── renew └── README.md ├── setup ├── Dockerfile ├── README.md └── setup ├── README.md └── LICENSE /docker-host-ipc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | COPY ./namedpipe /processor 3 | RUN chmod +x /processor 4 | CMD ["/processor"] 5 | -------------------------------------------------------------------------------- /auto-letsencrypt-cloudflare/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM certbot/dns-cloudflare:latest 2 | 3 | ADD ./scripts /scripts 4 | RUN chmod +x /scripts/cron && chmod +x /scripts/renew 5 | 6 | VOLUME /ipc 7 | 8 | ENTRYPOINT ["/scripts/cron"] 9 | -------------------------------------------------------------------------------- /setup/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | RUN apk update \ 3 | && apk add git jq curl \ 4 | && mkdir -p /app 5 | 6 | COPY setup /app/ 7 | RUN chmod +x /app/setup 8 | 9 | VOLUME /output 10 | 11 | ENV USERNAME="" 12 | ENV TOKEN="" 13 | 14 | WORKDIR /app 15 | 16 | CMD ["/bin/sh", "/app/setup"] -------------------------------------------------------------------------------- /auto-letsencrypt-cloudflare/scripts/cron: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$CRON" ]; then 4 | CRON="0 */12 * * *" 5 | fi 6 | 7 | TMP_CRON_FILE=$(mktemp) 8 | echo "$CRON /scripts/renew" > "$TMP_CRON_FILE" 9 | 10 | crontab "$TMP_CRON_FILE" 11 | 12 | rm "$TMP_CRON_FILE" 13 | 14 | /scripts/renew 15 | 16 | crond -f 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Containers 2 | 3 | This repository contains purpose and task-specific containers (mostly Docker). Each directory contains a specific container configuration with its own README, so check the specific directory of the container for more documentation. 4 | 5 | 1. [**auto-letsencrypt-cloudflare**](./auto-letsencrypt-cloudflare): Automated LetsEncrypt SSL certificate creation/renewal with post-processing hooks. 6 | 2. **[docker-host-ipc](./docker-host-ipc)**: Communicate with the Docker host from within a Docker container. 7 | 2. **[setup](./setup)**: Acquire setup scripts from GitHub, with a one-liner. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Author 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker-host-ipc/namedpipe: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cleanup() { 3 | echo "Received SIGINT, closing..." 4 | exit 0 5 | } 6 | 7 | trap cleanup TERM INT 8 | set -e 9 | 10 | if [ -z "$NAMED_PIPE" ]; then 11 | echo "using default pipe /ipc/channel" 12 | NAMED_PIPE=/ipc/channel 13 | fi 14 | 15 | if [ ! -p "$NAMED_PIPE" ]; then 16 | echo "creating named pipe ($NAMED_PIPE)" 17 | DIR_PATH=$(dirname "$NAMED_PIPE") 18 | mkdir -p "$DIR_PATH" 19 | chmod 777 "$DIR_PATH" 20 | mkfifo "$NAMED_PIPE" 21 | 22 | if [ -z "$MAX_LOG_LINES" ]; then 23 | MAX_LOG_LINES=25000 24 | fi 25 | fi 26 | 27 | echo "now monitoring $NAMED_PIPE" 28 | 29 | mkdir -p /tmp 30 | 31 | while true; do 32 | if [ -n "$LOG" ]; then 33 | DIR_PATH=$(dirname "$LOG") 34 | mkdir -p "$DIR_PATH" 35 | touch "$LOG" 36 | result=$(eval "$(cat "$NAMED_PIPE")" 2>&1) 37 | echo "$(date '+%Y-%m-%d %H:%M:%S') $result" >> "$LOG" 38 | tail -n $MAX_LOG_LINES "$LOG" > /tmp/tmp.log && mv /tmp/tmp.log "$LOG" 39 | else 40 | eval "$(cat $NAMED_PIPE)" 41 | fi 42 | done 43 | 44 | # cleanup() { 45 | # echo "Received SIGINT, closing..." 46 | # exit 0 47 | # } 48 | 49 | # trap cleanup TERM INT 50 | # set -e 51 | -------------------------------------------------------------------------------- /auto-letsencrypt-cloudflare/scripts/renew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # The domain to renew 3 | LIVE_PATH="/etc/letsencrypt/live/$DOMAIN" 4 | EMAIL=letsencrypt@$DOMAIN 5 | 6 | if [ -z "$DNS_PROPAGATION_SECONDS" ]; then 7 | DNS_PROPAGATION_SECONDS=10 8 | fi 9 | 10 | get_expire_time() { 11 | local cert_path="$1" 12 | local ds 13 | 14 | ds=$(openssl x509 -enddate -noout -in "$cert_path" | awk -F= '/notAfter/ {print $2}') 15 | date -u -D "%b %d %H:%M:%S %Y" -d "$(echo "$ds" | sed 's/GMT/UTC/')" '+%s' 16 | } 17 | 18 | get_create_time() { 19 | local cert_path="$1" 20 | local ds 21 | 22 | ds=$(openssl x509 -startdate -noout -in "$cert_path" | awk -F= '/notBefore/ {print $2}') 23 | date -u -D "%b %d %H:%M:%S %Y" -d "$(echo "$ds" | sed 's/GMT/UTC/')" '+%s' 24 | } 25 | 26 | # Check if the certificate exists 27 | if [ -L "$LIVE_PATH/cert.pem" ]; then 28 | # Get the expiration time of the certificate in seconds since epoch 29 | EXPIRATION_TIME=$(get_expire_time "$LIVE_PATH/cert.pem") 30 | CREATION_TIME=$(get_create_time "$LIVE_PATH/cert.pem") 31 | 32 | # Calculate the time until expiration in seconds 33 | TIME_UNTIL_EXPIRATION=$((EXPIRATION_TIME - CREATION_TIME)) 34 | 35 | # Calculate the time until expiration in hours 36 | TIME_UNTIL_EXPIRATION_HOURS=$((TIME_UNTIL_EXPIRATION / 3600)) 37 | 38 | # Check if the certificate will expire within the next 24 hours 39 | if [ "$TIME_UNTIL_EXPIRATION_HOURS" -ge 24 ]; then 40 | echo "$DOMAIN certificate renewal is unnecessary. It won't expire until $(openssl x509 -enddate -noout -in "$LIVE_PATH/cert.pem" | cut -d= -f2) ($TIME_UNTIL_EXPIRATION_HOURS hours)." 41 | if [ -f "$AFTER_ABORT" ]; then 42 | if [ -z "$IPC" ]; then 43 | cat "$AFTER_ABORT" > "$IPC" 44 | else 45 | chmod +x "$AFTER_ABORT" 46 | "$AFTER_ABORT" 2>&1 47 | fi 48 | else 49 | if [ ! -z "$AFTER_ABORT" ]; then 50 | if [ -z "$IPC" ]; then 51 | echo "$AFTER_ABORT" > "$IPC" 52 | else 53 | "$AFTER_ABORT" 2>&1 54 | fi 55 | fi 56 | fi 57 | exit 0 58 | fi 59 | fi 60 | 61 | # Generate if no cert exists or if an existing cert needs to be renewed within 24 hours. 62 | echo "Generating/renewing certificate for $DOMAIN: (takes minimum $DNS_PROPAGATION_SECONDS seconds)" 63 | 64 | # Create the Cloudflare credentials 65 | mkdir /secure 66 | if [ ! -f "/secure/cloudflare_credentials.ini" ]; then 67 | if [ -z "$CLOUDFLARE_API_TOKEN" ]; then 68 | echo "Cloudflare API token not found. Cannot create/renew $DOMAIN certificate" 69 | exit 1 70 | fi 71 | 72 | echo "dns_cloudflare_api_token = $CLOUDFLARE_API_TOKEN" > /secure/cloudflare_credentials.ini 73 | fi 74 | 75 | chmod 600 /secure/cloudflare_credentials.ini 76 | 77 | # Generate/renew certificate 78 | certbot certonly \ 79 | --dns-cloudflare \ 80 | --dns-cloudflare-credentials /secure/cloudflare_credentials.ini \ 81 | --email $EMAIL \ 82 | --register-unsafely-without-email \ 83 | --agree-tos \ 84 | --renew-by-default \ 85 | --non-interactive \ 86 | -d $DOMAIN 87 | 88 | # Check if the certificate exists 89 | if [[ -L "$LIVE_PATH/cert.pem" ]]; then 90 | echo "Validating $LIVE_PATH/cert.pem" 91 | # Get the expiration time of the certificate in seconds since epoch 92 | EXPIRATION_TIME=$(get_expire_time "$LIVE_PATH/cert.pem") 93 | CREATION_TIME=$(get_create_time "$LIVE_PATH/cert.pem") 94 | 95 | # Calculate the time until expiration in seconds 96 | TIME_UNTIL_EXPIRATION=$((EXPIRATION_TIME - CREATION_TIME)) 97 | 98 | # Calculate the time until expiration in hours 99 | TIME_UNTIL_EXPIRATION_HOURS=$((TIME_UNTIL_EXPIRATION / 3600)) 100 | 101 | # Check if the certificate will expire within the next 24 hours 102 | if [ "$TIME_UNTIL_EXPIRATION_HOURS" -ge 24 ]; then 103 | cat $LIVE_PATH/fullchain.pem $LIVE_PATH/privkey.pem > $LIVE_PATH/trusted.pem 104 | cat $LIVE_PATH/fullchain.pem > $LIVE_PATH/fullchain.crt 105 | cat $LIVE_PATH/chain.pem > $LIVE_PATH/ca.pem 106 | cat $LIVE_PATH/cert.pem > $LIVE_PATH/certificate.pem 107 | cat $LIVE_PATH/privkey.pem > $LIVE_PATH/private.key 108 | 109 | echo "$DOMAIN certificate renewal complete." 110 | 111 | if [ -f "$AFTER_SUCCESS" ]; then 112 | if [ -z "$IPC" ]; then 113 | cat "$AFTER_SUCCESS" > "$IPC" 114 | else 115 | chmod +x "$AFTER_SUCCESS" 116 | "$AFTER_SUCCESS" 2>&1 117 | fi 118 | else 119 | if [ ! -z "$AFTER_SUCCESS" ]; then 120 | if [ -z "$IPC" ]; then 121 | echo "$AFTER_SUCCESS" > "$IPC" 122 | else 123 | "$AFTER_SUCCESS" 2>&1 124 | fi 125 | fi 126 | fi 127 | 128 | exit 0 129 | fi 130 | fi 131 | 132 | echo "Could not find a recently created/updated $DOMAIN certificate at $LIVE_PATH" 133 | exit 1 134 | -------------------------------------------------------------------------------- /docker-host-ipc/README.md: -------------------------------------------------------------------------------- 1 | # Docker Host IPC 2 | 3 | This Linux-only container establishes a named pipe that other Docker containers can use to communicate with the Docker host. This requires the use of the `--privileged` flag. Conceptually, this image configures a named pipe that accepts commands, runs them, and (optionally) logs stdout/stderr to a file. 4 | 5 |
Why would you do this?
There are a few different ways for Docker containers to communicate with the host machine. Most are unnecessarily complex, such as using SSH to login to the host. This is a lot of overhead for communication that exists within a single server. It's also possible to setup a web server on the host to serve as a "bridge". That creates even more overhead than SSH communication.

It is far easier, with significantly less overhead/latency, to use a named pipe to communicate. It's not the kind of thing most developers remember how to do off the top of their head though. This image is designed for super-easy implementation while remaning as lightweight as lightest-weight as possible and restricting privileged use to an absolute minimum in a single container
6 | 7 | A named pipe is established on the Docker _host_, which can be mapped to other Docker containers that utilize the pipe for interprocess communications. 8 | 9 | **Run container...** 10 | 11 | ```sh 12 | docker run --rm \ 13 | --restart always \ 14 | --privileged \ 15 | -e "NAMED_PIPE=/host_path/to/named/pipe" \ 16 | -e "LOG=/host_path/to/pipe.log" \ 17 | author/docker-host-ipc 18 | ``` 19 | 20 | _or as part of docker compose_... 21 | 22 | ```sh 23 | version: '3' 24 | services: 25 | docker-ipc: 26 | image: author/docker-host-ipc 27 | privileged: true 28 | restart: always 29 | environment: 30 | NAMED_PIPE: "/host_path/to/named/pipe" 31 | LOG: "/host_path/to/pipe.log" 32 | ``` 33 | 34 | A common pattern we've found to be effective is to create a directory on the host called `/ipc`, then run the docker container as follows: 35 | 36 | ```shell 37 | mkdir -p /ipc 38 | ``` 39 | 40 | ```sh 41 | docker run --rm -d \ 42 | --name ipc \ 43 | --privileged \ # required 44 | -v "/ipc:/ipc" \ # required 45 | -e "NAMED_PIPE=/ipc/channel" \ # This line isn't technically necessary since the default is /ipc/channel. It is shown here to illustrate how the container works so you can choose a different named socket (i.e. other than /ipc/channel) if you prefer. 46 | -e "LOG=/ipc/channel.log" \ # Optional audit log 47 | author/docker-host-ipc 48 | ``` 49 | 50 | This establishes a named pipe *on the host* at `/ipc/channel` and a log file available at `/ipc/channel.log`. 51 | 52 | **Environment Variables** 53 | 54 | | Variable | Required | Default | Description | 55 | | :---------------- | :------: | :--------------- | :------------------------------------------------------------------ | 56 | | `NAMED_PIPE` | No | `/ipc/channel` | The location of the named pipe. | 57 | | `LOG` | No | - | The location of the log file. | 58 | | `MAX_LOG_LINES` | No | `25000` | Truncate the log after this many lines (removes oldest lines first) | 59 | 60 | **Volumes** 61 | 62 | | Target | Required | Purpose | 63 | | :------- | :-----------: | :------------------------------------------------ | 64 | | `/ipc` | **Yes** | The_host_ directory where the named pipe resides. | 65 | 66 | ## Using the Named Pipe for IPC Within Docker Containers 67 | 68 | Other containers can utilize the pipe once it is created. Consider the following example Docker image: 69 | 70 | `Dockerfile` (built as `example`) 71 | 72 | ```sh 73 | FROM busybox 74 | VOLUME /ipc 75 | RUN echo "ls -l" > /ipc/channel 76 | ``` 77 | 78 | This would be run as follows: 79 | 80 | ```sh 81 | docker run --rm -v "/ipc:/ipc" -e "LOG=/ipc/channel.log" example 82 | ``` 83 | 84 | The result of `ls -l` (line 4 of Dockerfile) will be available in `/ipc/channel.log` on the host, which will look similar to: 85 | 86 | ```shell 87 | b 88 | ``` 89 | 90 | Notice the logs are prefixed with the current timetamp. 91 | 92 | This is a trivial/contrived example, but it highlights how commands can be executed _on the Docker host_*from within a Docker container*. 93 | 94 | ## Motivation 95 | 96 | This image was initially created to support [auto-letsencrypt-cloudflare](../auto-letsencrypt-cloudflare/README.md). When SSL certificates are renewed, commands are issued to the Docker host to restart containers of the affected services. This prevents the need for other containers to have privileges, making this a more secure option for lightweight interprocess communication within a collection of Docker containers running on the same server. 97 | 98 | --- 99 | 100 | Copyright (c) 2024 Author Software, Inc. All rights reserved, subject to the MIT License. 101 | -------------------------------------------------------------------------------- /setup/README.md: -------------------------------------------------------------------------------- 1 | # Setup Script 2 | 3 | This Docker image helps acquire content from Github repositories (both public and private). It provides a quick and secure way to retrieve infrastructure assets without having to configure git, SSH, FTP, or other utilities. The intention is to help rapidly bootstrap common infrastructure in a Docker environment with a short and memorable command. 4 | 5 | The command assumes a source repository contains setup scripts and assets in one of two arrangements: Directory-Based or Branch-Based. 6 | 7 | **Directory-Based Configurations** 8 | The target repository must be arranged with each top level directory representing a configuration. 9 | 10 | ```sh 11 | myorg/repo 12 | ├── discovery 13 | │ ├── docker-compose.yml 14 | │ └── setup.sh 15 | ├── ldap 16 | │ ├── docker-compose.yml 17 | │ └── setup.sh 18 | ├── web 19 | │ ├── docker-compose.yml 20 | │ └── setup.sh 21 | ├── api 22 | │ ├── docker-compose.yml 23 | │ └── setup.sh 24 | └── database 25 | ├── docker-compose.yml 26 | └── setup.sh 27 | ``` 28 | 29 | **Branch-Based Configuration** 30 | In this arrangement, each branch represents a configuration. Source files are assumed to exist within the branch. 31 | 32 | ```sh 33 | myorg/repo branches 34 | ├── main 35 | ├── discovery 36 | ├── ldap 37 | ├── web 38 | ├── api 39 | └── database 40 | ``` 41 | 42 | ## Memorable Command 43 | 44 | ```sh 45 | docker run --rm -it \ 46 | -e "GITHUB_USER=github_username" \ 47 | -e "GITHUB_TOKEN=github_personal_access_token" \ 48 | -e "GITHUB_ORG=organization" \ # optional 49 | -v "./:/output" \ 50 | author/setup 51 | ``` 52 | 53 | This command has two required environment variables (Github user and token) and one optional environment variable (Github organization). The volume is also required. The mounted volume is where Github files are downloaded to on the host. In this example, we use the relative `./` directory, which will load the files into the current working directory. 54 | 55 | ### Usage 56 | 57 | This command automatically pulls the container from Docker Hub and runs a simple prompt wizard. If you specfiy a Github Organization, a list of repositories will be presented to choose from. 58 | 59 | ```sh 60 | Available repositories: 61 | 1. myorg/discovery 62 | 2. myorg/ldap 63 | 3. myorg/web 64 | 4. myorg/api 65 | 5. myorg/database 66 | 67 | Enter the number of the repo you wish to setup: 68 | ``` 69 | 70 | If no organization is specified, you will be prompted to manually input the org/repository you wish to connect to. 71 | 72 | Next, the wizard prompts to determine whether you want to use the `Branch` or `Directory` method for retrieving content. 73 | 74 | ```sh 75 | Using author/infrastructure 76 | 77 | Setup from: 78 | 1. Branch 79 | 2. Directory 80 | q. Quit 81 | 82 | Enter the number of the repo arrangement: 83 | ``` 84 | 85 | If `Branch` is selected, a prompt will appear to select the branch: 86 | 87 | ```sh 88 | Select a branch: 89 | 1. infrastructure-dev 90 | 2. infrastructure-prod 91 | 3. main 92 | q. Quit 93 | 94 | Enter the number of your choice: 95 | ``` 96 | 97 | If `Directory` is selected, the top level directories of the repository will be listed, allowing selection of the directory to download. 98 | 99 | ```sh 100 | Retrieving myorg/containers top level directories... 101 | 102 | Select a directory or Quit: 103 | 1. auto-letsencrypt-cloudflare 104 | 2. docker-host-ipc 105 | q. Quit 106 | 107 | Enter the number of your choice: 108 | ``` 109 | 110 | Once a branch or directory is selected, the wizard downloads the contents to the container's `/app` directory (which is mapped to the host container). 111 | 112 | When complete, the container will exit and remove itself. 113 | 114 | **Tip* 115 | If you wish to remove the image after the wizard is complete, add `&& docker rmi author/setup` to the end of the command. For example: 116 | 117 | ```sh 118 | docker run --rm -it \ 119 | -e "GITHUB_USER=github_username" \ 120 | -e "GITHUB_TOKEN=github_personal_access_token" \ 121 | -e "GITHUB_ORG=organization" \ # optional 122 | -v "./:/output" \ 123 | author/setup \ 124 | && docker rmi author/setup 125 | ``` 126 | 127 | We recommend doing this the last time you plan to run the command. The image is only relevant to setup and is unncessary once setup is complete. 128 | 129 | ### Next Steps 130 | 131 | It is assumed some sort of scripts are now available. In our practice, we create setup wizards for each type of infrastructure component we deploy. These are stored in a file called `setup`. This way, no matter which server we're configuring, there is a common file to setup/install what we need. Sometimes this script is as simple as `docker compose up -d`. Other times we have a wizard that generates a Dockerfile from a series of questions, or commands to install dependencies. 132 | 133 | As a result, we use the following command and attempt to commit it to memory: 134 | 135 | ```sh 136 | docker run --rm -it \ 137 | -e "GITHUB_USER=github_username" \ 138 | -e "GITHUB_TOKEN=github_personal_access_token" \ 139 | -e "GITHUB_ORG=organization" \ 140 | -v "./:/output" \ 141 | author/setup \ 142 | && docker rmi author/setup \ 143 | && chmod +x ./setup \ 144 | && ./setup 145 | ``` 146 | 147 | This command downloads setup files, prepares permissions, and executes the `setup` file. As a result, administrators are greeted with a series of easy-to-answer prompts that automate the setup of our environments. 148 | 149 | ## Why? 150 | 151 | There are situations where the overhead of tools like Kubernetes (and even K3S) don't justify the extra effort. It can be easier to maintain configurations and updates in a single git repository as opposed to needing a database/consul/etcd. Bottom line: sometimes the simplest solution is just a series of scripts. 152 | -------------------------------------------------------------------------------- /auto-letsencrypt-cloudflare/README.md: -------------------------------------------------------------------------------- 1 | # auto-letsencrypt-cloudflare 2 | 3 | This "always-on" Linux-only container keeps LetsEncrypt TLS/SSL certificates up to date. Post-create/renew hooks and physical file mirrors differentiate this container from other LetsEncrypt tools. 4 | 5 | ## Key Features: 6 | 7 | - **Always-on** (cron) runs the update process on a regular schedule (configurable). 8 | - **Hooks**: Optionally runs a user-defined script after successful certificate creation/renewal. 9 | - **Cloudflare DNS Verification** 10 | - **Physical Files**: For times when an application cannot read LetsEncrypt symlinks (see "Alternative to LetsEncrypt Symlinks" below). 11 | 12 | ### Example Use cases: 13 | 14 | - Auto-refresh LDAP certificates and reload the LDAP server upon completion. 15 | - Auto-renew Node.js certificates. 16 | - Auto-refresh a secure SMTP server. 17 | - Trigger webhooks when certificates are updated. 18 | - Send email notifications when certificates are updated. 19 | 20 | While it is possible to use this to refresh certificates for HTTP servers, there are many other dedicated tools better designed for that specific purpose (Caddy, NGINX, etc). 21 | 22 | ## Running in Docker 23 | 24 | **Environment Variables** 25 | 26 | | Name | Required | Default | Description | 27 | | :--------------------------------- | :-----------: | :--------------: | :------------------------------------------------------------------------------------------------------------- | 28 | | *`AFTER_SUCCESS` | No | - | The command or path of the script_on the host_ to run after the successful creation/renewal of certificates. | 29 | | `DNS_PROPAGATION_SECONDS` | No | `10` | Number of seconds to allow for DNS changes before LetsEncrypt verifies the DNS entries. | 30 | | **`DOMAIN`** | **Yes** | - | The domain to create/renew LetsEncrypt certificates for | 31 | | **`CLOUDFLARE_API_TOKEN`** | **Yes** | - | The Cloudflare API token. This token must permissions to create DNS entries. | 32 | | `CRON` | No | `0 */12 * * *` | The schedule to run the renewal process on. Default is every 12 hours. | 33 | | *`AFTER_ABORT` | No | - | The command or path of the script_on the host_ to run if a certificate is not created/renewed. 34 | | `IPC` | No | - | The IPC channel. | 35 | 36 | *Scripts reside on the host, not within the Docker container. They are executed via a [docker-host-ipc](../docker-host-ipc) channel between the container and the host. 37 | 38 | **Volumes** 39 | 40 | | Target | Purpose | 41 | | :--------- | :----------------------------------------------------- | 42 | | `/etc/letsencrypt` | Maps to the LetsEncrypt directory. **(REQUIRED)** | 43 | | `/ipc` | Maps to the named pipe. **(SEE NOTES)** | 44 | 45 | **Notes** 46 | If you wish to run scripts in response to creation/renewal (or lack thereof), you can run them in the container or on the host. Running within the container will isolate the script to the container. This is useful for running scripts that trigger webhooks, remote processes, etc. However; the script is limited to running in the confines of the container. There are circumstances where you may wish for the script to run on the Docker host instead of within the Docker container. There are two ways to do this. 47 | 48 | 1. Use the `--privileged` flag when running the container. This is a brute force approach that grants the container significant privileges (the same as the host kernel). It should be used with caution, in trusted environments only. 49 | 2. Use an IPC channel. This container will look for a named pipe at `/ipc` and pass commands to the named pipe, allowing them to run on the host. This requires additional configuration on the host. See the "IPC Communication" section for instructions. 50 | 51 | _Example:_ 52 | 53 | ```sh 54 | docker run --rm \ 55 | --restart unless-stopped \ 56 | -e "DOMAIN=my.domain.com" \ 57 | -e "CLOUDFLARE_API_TOKEN=mytoken" \ 58 | -e "CRON=* 0/12 * * * *" \ 59 | -e "AFTER_SUCCESS=/path/to/scriptname" \ 60 | -e "AFTER_ABORT=/path/to/scriptname" \ 61 | -e "IPC=/ipc/channel" \ 62 | -v "/ipc:/ipc" \ # Maps the docker-host-ipc channel directory 63 | -v "/etc/letsencrypt:/etc/letsencrypt" \ 64 | author/autorenew-letsencrypt-cloudflare 65 | ``` 66 | 67 | _or as part of a docker-compose file..._ 68 | 69 | ```sh 70 | version: '3' 71 | services: 72 | letsencrypt: 73 | image: author/autorenew-letsencrypt-cloudflare:latest 74 | container_name: mydomain_ssl_renewer 75 | environment: autorenew-letsencrypt-cloudflare 76 | DOMAIN: my.domain.com 77 | CLOUDFLARE_API_TOKEN: mytoken 78 | CRON: "0 */12 * * *" 79 | AFTER_SUCCESS: /handlers/scriptname 80 | AFTER_ABORT: /handlers/scriptname 81 | IPC: /ipc/channel 82 | volumes: 83 | /path/to/letsencrypt:/etc/letsencrypt 84 | /ipc:/ipc 85 | restart: unless-stopped 86 | ``` 87 | 88 | ## Alernative to LetsEncrypt Symlinks 89 | 90 | LetsEncrypt generates pretty file names like `cert.pem` and `fullchain.pem`. However, these are symlinks to the most recently archived certificate assets, not true files. For example: 91 | 92 | `/etc/letsencrypt/live/my.domain.com/cert.pem -> /etc/letsencrypt/archive/my.domain.com/cert1.pem` 93 | 94 | Some applications cannot reliably read symlinks. Using the archive file directly poses a problem because the filename changes every time the certificate is renewed. This container makes a copy of the assets as physical files. Each time the certificate is renewed, the copy is recreated, assuring the same file name is available for each physical file. This avoids. 95 | 96 | In addition to the standard LetsEncrypt symlinks, the following files are generated in the `/etc/letsencrypt/live/` directory: 97 | 98 | | Physcial File | LetsEncrypt Equivalent | 99 | | :------------------ | :--------------------------------------------------------- | 100 | | `trusted.pem` | `fullchain.pem` + `privkey.pem`(concatenated into one) | 101 | | `fullchain.crt` | `fullchain.pem` | 102 | | `ca.pem` | `chain.pem` | 103 | | `certificate.pem` | `cert.pem` | 104 | | `private.key` | `privkey.pem` | 105 | 106 | ## IPC Communication 107 | 108 | In some cases, you may want to run commands on the host after a certificate is created/renewed. For example, you may wish to use LetsEncrypt certificates to secure an LDAP server running in another container . When the certificates are renewed, you may need to restart the LDAP Docker container or execute a command using `docker exec` on the LDAP container. This can only be done from the Docker host, not within an unprivileged Docker container. 109 | 110 | This container supports a [docker-host-ipc](../docker-host-ipc) channel (`/ipc`) to allow the execution of scripts/commands after the creation/renewal of certificates completes (or abort). This channel can be mapped into the `/ipc` volume of this container, allowing post-creation/renewal commands/scripts to run on the host. 111 | 112 | --- 113 | 114 | Copyright (c) 2024 Author Software, Inc. 115 | -------------------------------------------------------------------------------- /setup/setup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Function to download the contents of a directory 4 | download_directory() { 5 | local repo=$1 6 | local directory=$2 7 | 8 | mkdir -p /app/tmp 9 | cd /app/tmp 10 | if [ -p ".git" ]; then 11 | rm -r .git 12 | fi 13 | git config --global init.defaultBranch main 14 | git init 15 | git remote add -f origin https://$GITHUB_USER:$GITHUB_TOKEN@github.com/$repo.git 16 | git config core.sparsecheckout true 17 | echo $directory/ >> .git/info/sparse-checkout 18 | git pull origin main 19 | 20 | mv -if /app/tmp/$directory/* /output 21 | rm -rf /app/tmp 22 | 23 | echo "Download completed. Contents are in /app" 24 | ls -l /output 25 | } 26 | 27 | # Function to download the contents of a branch 28 | download_branch() { 29 | local repo=$1 30 | local branch=$2 31 | 32 | mkdir -p /app/tmp 33 | cd /app/tmp 34 | if [ -p ".git" ]; then 35 | rm -r .git 36 | fi 37 | git config --global init.defaultBranch $branch 38 | git init 39 | git remote add -f origin https://$GITHUB_USER:$GITHUB_TOKEN@github.com/$repo.git 40 | git config core.sparsecheckout true 41 | set -x 42 | git pull origin $branch 43 | 44 | mv -if /app/tmp/$directory/* /output 45 | rm -rf /app/tmp 46 | 47 | echo "Download completed. Contents are in /app" 48 | ls -l /output 49 | } 50 | 51 | # Prompt user for GitHub repository details 52 | if [ ! -z "$GITHUB_ORG" ]; then 53 | API_URL="https://api.github.com/orgs/$GITHUB_ORG/repos" 54 | response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" $API_URL) 55 | 56 | # Check if the response is an array and not empty 57 | if [[ "$(echo "$response" | jq 'length > 0')" == "true" ]]; then 58 | # Extract repository names from the response using jq 59 | repo_names=$(echo "$response" | jq -r ".[].name") 60 | 61 | # Display the list of repository names 62 | echo "Available repositories:" 63 | count=1 64 | for r in $repo_names; do 65 | echo " $count. $GITHUB_ORG/$r" 66 | count=$((count+1)) 67 | done 68 | echo " q. Quit" 69 | echo "" 70 | 71 | while true; do 72 | read -p "Enter the number of the repo to setup: " choice 73 | case $choice in 74 | [1-9]*) 75 | if [ $choice -le $count ]; then 76 | selected=$(echo $repo_names | awk "{print \$"$choice"}") 77 | echo "" 78 | echo "Using $GITHUB_ORG/$selected" 79 | repo="$GITHUB_ORG/$selected" 80 | break 81 | else 82 | echo "Invalid choice. Please enter a valid number." 83 | fi 84 | ;; 85 | "q" | "Q") 86 | echo "setup aborted" 87 | break 88 | ;; 89 | *) 90 | echo "Invalid choice. Please enter a valid number." 91 | ;; 92 | esac 93 | done 94 | else 95 | echo "No repositories found in $GITHUB_ORG accessible by $GITHUB_USER." 96 | exit 0 97 | fi 98 | else 99 | read -p "Repository: " repo 100 | # Verify repo exists 101 | chk_repo_url="https://api.github.com/repos/$repo" 102 | response=$(curl -H "Authorization: token $GITHUB_TOKEN" -s -o /dev/null -w "%{http_code}" "$chk_repo_url") 103 | if [ "$response" -eq 404 ]; then 104 | echo "Repository '$repo' does not exist." 105 | exit 1 106 | fi 107 | fi 108 | 109 | # Prompt for setup type 110 | echo "" 111 | echo "Setup from:" 112 | echo " 1. Branch" 113 | echo " 2. Directory" 114 | echo " q. Quit" 115 | echo "" 116 | while true; do 117 | read -p "Enter the number of the repo arrangement: " choice 118 | case $choice in 119 | 1) 120 | # Fetch remote branch names 121 | branch_names=$(git ls-remote --heads "https://$GITHUB_USER:$GITHUB_TOKEN@github.com/$repo.git" | awk -F/ '{print $NF}') 122 | 123 | echo "" 124 | echo "Select a branch:" 125 | count=1 126 | for dir in $branch_names; do 127 | echo " $count. $dir" 128 | count=$((count+1)) 129 | done 130 | echo " q. Quit" 131 | echo "" 132 | 133 | while true; do 134 | read -p "Enter the number of your choice: " choice 135 | case $choice in 136 | [1-9]*) 137 | if [ $choice -le $count ]; then 138 | selected=$(echo $branch_names | awk "{print \$"$choice"}") 139 | echo "You selected branch: $selected" 140 | download_branch $repo $selected 141 | exit 0 142 | else 143 | echo "Invalid choice. Please enter a valid number." 144 | fi 145 | ;; 146 | "q" | "Q") 147 | echo "setup aborted" 148 | break 149 | ;; 150 | *) 151 | echo "Invalid choice. Please enter a valid number." 152 | ;; 153 | esac 154 | done 155 | ;; 156 | 2) 157 | repo_url="https://api.github.com/repos/$repo/contents" 158 | 159 | # Use curl to make a request to the GitHub API 160 | echo "Retrieving $repo top level directories..." 161 | response=$(curl -s -u "$GITHUB_USER:$GITHUB_TOKEN" $repo_url) 162 | 163 | # Check if the request was successful 164 | if [ $? -ne 0 ]; then 165 | echo "Failed to retrieve repository contents. Please check your credentials and repository information." 166 | exit 1 167 | fi 168 | 169 | # Check if the response is an error 170 | # Check if the response is an array 171 | if [ "$(echo "$response" | jq 'type')" = "array" ]; then 172 | # Check if the array has any elements 173 | if [ "$(echo "$response" | jq 'length')" -gt 0 ]; then 174 | # Check if the first element of the array has a "message" key 175 | if [ "$(echo "$response" | jq -r '.[0] | has("message")')" = "true" ]; then 176 | # Extract and print the error message 177 | err_msg=$(echo "$response" | jq -r '.[0].message') 178 | echo "Error: $err_msg" 179 | exit 1 180 | fi 181 | fi 182 | fi 183 | 184 | # Extract directory names from the API response 185 | directories=$(echo $response | jq -r '.[] | select(.type == "dir") | .name') 186 | 187 | # Check if there are any directories 188 | if [ -z "$directories" ]; then 189 | echo "No directories found in the main branch." 190 | exit 1 191 | fi 192 | 193 | # Display the menu 194 | echo "" 195 | echo "Select a directory or Quit:" 196 | count=1 197 | for dir in $directories; do 198 | echo " $count. $dir" 199 | count=$((count+1)) 200 | done 201 | echo " q. Quit" 202 | echo "" 203 | 204 | # Prompt the user to select a directory 205 | while true; do 206 | read -p "Enter the number of your choice: " choice 207 | case $choice in 208 | [1-9]*) 209 | if [ $choice -le $count ]; then 210 | selected_dir=$(echo $directories | awk "{print \$"$choice"}") 211 | echo "You selected directory: $selected_dir" 212 | download_directory $repo $selected_dir 213 | exit 0 214 | else 215 | echo "Invalid choice. Please enter a valid number." 216 | fi 217 | ;; 218 | "q" | "Q") 219 | echo "setup aborted" 220 | break 221 | ;; 222 | *) 223 | echo "Invalid choice. Please enter a valid number." 224 | ;; 225 | esac 226 | done 227 | ;; 228 | "q" | "Q") 229 | echo "setup aborted" 230 | exit 0 231 | ;; 232 | *) 233 | echo "Invalid choice. Please enter a valid number." 234 | esac 235 | done 236 | --------------------------------------------------------------------------------