NOISSUE - Add a fast Certbot startup (#3517)

Signed-off-by: dusan <borovcanindusan1@gmail.com>
This commit is contained in:
Dušan Borovčanin
2026-05-20 19:23:19 +02:00
committed by GitHub
parent 683809dc6b
commit 353e050a39
10 changed files with 411 additions and 8 deletions
+12 -1
View File
@@ -144,7 +144,7 @@ FILTERED_SERVICES = $(filter-out $(RUN_ADDON_ARGS), $(SERVICES))
all: $(SERVICES)
.PHONY: all $(SERVICES) dockers dockers_dev latest release run_latest run_stable run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
.PHONY: all $(SERVICES) dockers dockers_dev latest release run_latest run_tls run_stable run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
clean:
rm -rf ${BUILD_DIR}
@@ -293,6 +293,17 @@ run_latest: check_certs
$(SED_INPLACE) 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=latest/' docker/.env
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
run_tls:
@test -n "$(host)" || (echo "Usage: make run_tls host=example.com [email=admin@example.com] [letsencrypt=false] [staging=false] [force=true]" && exit 2)
@if [ "$(or $(letsencrypt),true)" != "false" ] && [ -z "$(email)" ]; then echo "Usage: make run_tls host=example.com email=admin@example.com [letsencrypt=false] [staging=false] [force=true]"; exit 2; fi
MG_PUBLIC_HOST="$(host)" \
MG_LETSENCRYPT_ENABLED="$(or $(letsencrypt),true)" \
MG_LETSENCRYPT_EMAIL="$(email)" \
MG_LETSENCRYPT_STAGING="$(or $(staging),true)" \
MG_LETSENCRYPT_FORCE_RENEWAL="$(or $(force),false)" \
DOCKER_PROJECT="$(DOCKER_PROJECT)" \
./docker/setup-tls.sh
run_stable: check_certs
$(eval version = $(shell git describe --abbrev=0 --tags))
git checkout $(version)
+9
View File
@@ -7,12 +7,21 @@
GRPC_MTLS=
## NginX
MG_PUBLIC_HOST=localhost
MG_UI_HOST=ui
MG_LETSENCRYPT_ENABLED=false
MG_LETSENCRYPT_EMAIL=
MG_LETSENCRYPT_STAGING=true
MG_LETSENCRYPT_FORCE_RENEWAL=false
MG_NGINX_HTTP_PORT=80
MG_NGINX_SSL_PORT=443
MG_NGINX_MQTT_PORT=1883
MG_NGINX_MQTTS_PORT=8883
MG_NGINX_AMQP_PORT=5682
MG_NGINX_SERVER_NAME=
# After issuing a Let's Encrypt certificate, uncomment these paths and restart nginx.
# MG_NGINX_SERVER_CERT=./ssl/letsencrypt/live/${MG_PUBLIC_HOST}/fullchain.pem
# MG_NGINX_SERVER_KEY=./ssl/letsencrypt/live/${MG_PUBLIC_HOST}/privkey.pem
## FluxMQ Cluster
MG_FLUXMQ_IMAGE_TAG=latest
+62 -7
View File
@@ -129,16 +129,71 @@ services:
Nginx is the entry point for all traffic to Magistrala.
By using environment variables file at `docker/.env` you can modify the below given Nginx directive.
| Environment Variable | Description |
|----------------------|-------------|
| `MG_NGINX_SERVER_NAME` | `MG_NGINX_SERVER_NAME` environmental variable is used to configure nginx directive `server_name`. If environmental variable `MG_NGINX_SERVER_NAME` is empty then default value `localhost` will set to `server_name`. |
| `MG_NGINX_SERVER_CERT` | `MG_NGINX_SERVER_CERT` environmental variable is used to configure nginx directive `ssl_certificate`. If environmental variable `MG_NGINX_SERVER_CERT` is empty then by default server certificate in the path `docker/ssl/certs/magistrala-server.crt` will be assigned. |
| `MG_NGINX_SERVER_KEY` | `MG_NGINX_SERVER_KEY` environmental variable is used to configure nginx directive `ssl_certificate_key`. If environmental variable `MG_NGINX_SERVER_KEY` is empty then by default server certificate key in the path `docker/ssl/certs/magistrala-server.key` will be assigned. |
| `MG_NGINX_SERVER_CLIENT_CA` | `MG_NGINX_SERVER_CLIENT_CA` environmental variable is used to configure nginx directive `ssl_client_certificate`. If environmental variable `MG_NGINX_SERVER_CLIENT_CA` is empty then by default certificate in the path `docker/ssl/certs/ca.crt` will be assigned. |
| `MG_NGINX_SERVER_DHPARAM` | `MG_NGINX_SERVER_DHPARAM` environmental variable is used to configure nginx directive `ssl_dhparam`. If environmental variable `MG_NGINX_SERVER_DHPARAM` is empty then by default file in the path `docker/ssl/dhparam.pem` will be assigned. |
| Environment Variable | Description |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MG_PUBLIC_HOST` | Public DNS name for the Docker host. This value is used by UI URLs and Let's Encrypt certificate requests. |
| `MG_UI_HOST` | Internal Compose hostname for the UI service. Defaults to `ui`. |
| `MG_LETSENCRYPT_ENABLED` | Set to `true` to request and use a Let's Encrypt certificate. Set to `false` to comment out the Let's Encrypt cert/key paths and use the fallback Nginx certificate. |
| `MG_LETSENCRYPT_EMAIL` | Email address used by Let's Encrypt for expiry and account notifications. Required when running the `letsencrypt` profile. |
| `MG_LETSENCRYPT_STAGING` | Set to `true` to request staging certificates while testing. Set to `false` for trusted production certificates. |
| `MG_LETSENCRYPT_FORCE_RENEWAL` | Set to `true` for one certbot run when replacing a staging certificate with a production certificate. Set it back to `false` after the production certificate is issued. |
| `MG_NGINX_SERVER_NAME` | `MG_NGINX_SERVER_NAME` environmental variable is used to configure nginx directive `server_name`. If environmental variable `MG_NGINX_SERVER_NAME` is empty then default value `localhost` will set to `server_name`. |
| `MG_NGINX_SERVER_CERT` | `MG_NGINX_SERVER_CERT` environmental variable is used to configure nginx directive `ssl_certificate`. If environmental variable `MG_NGINX_SERVER_CERT` is empty then by default server certificate in the path `docker/ssl/certs/magistrala-server.crt` will be assigned. |
| `MG_NGINX_SERVER_KEY` | `MG_NGINX_SERVER_KEY` environmental variable is used to configure nginx directive `ssl_certificate_key`. If environmental variable `MG_NGINX_SERVER_KEY` is empty then by default server certificate key in the path `docker/ssl/certs/magistrala-server.key` will be assigned. |
| `MG_NGINX_SERVER_CLIENT_CA` | `MG_NGINX_SERVER_CLIENT_CA` environmental variable is used to configure nginx directive `ssl_client_certificate`. If environmental variable `MG_NGINX_SERVER_CLIENT_CA` is empty then by default certificate in the path `docker/ssl/certs/ca.crt` will be assigned. |
| `MG_NGINX_SERVER_DHPARAM` | `MG_NGINX_SERVER_DHPARAM` environmental variable is used to configure nginx directive `ssl_dhparam`. If environmental variable `MG_NGINX_SERVER_DHPARAM` is empty then by default file in the path `docker/ssl/dhparam.pem` will be assigned. |
Adjust these values in `.env` to configure TLS / SSL behavior for your deployment.
### HTTPS UI with Let's Encrypt
The Compose stack can request and renew a Let's Encrypt certificate with the optional `letsencrypt` profile. This secures the public Nginx entrypoint and serves the UI through `https://${MG_PUBLIC_HOST}/`. Plain UI requests to `/` are redirected to HTTPS, while API and messaging routes keep their existing protocol behavior. Certbot stores challenge files and issued certificates under ignored local paths in `docker/ssl/`.
Prerequisites:
- `MG_PUBLIC_HOST` must resolve to the Docker host.
- Ports `80` and `443` must be reachable from the public internet.
- Set `MG_LETSENCRYPT_EMAIL` before requesting a certificate.
For a staging certificate, run one command from the project root:
```bash
make run_tls host=example.com email=admin@example.com
```
For a trusted production certificate, set `staging=false`:
```bash
make run_tls host=example.com email=admin@example.com staging=false
```
The target updates `docker/.env`, starts the Compose stack with the fallback certificate, runs certbot, switches Nginx to the issued certificate, and recreates Nginx. It also sets `MG_UI_DOCKER_ACCEPT_EULA=yes` for the UI container and configures public UI URLs to `https://${MG_PUBLIC_HOST}`.
To configure the same instance without Let's Encrypt, use:
```bash
make run_tls host=example.com letsencrypt=false
```
That command updates `docker/.env`, comments out `MG_NGINX_SERVER_CERT` and `MG_NGINX_SERVER_KEY`, stops certbot if it exists, and runs the stack with the fallback Nginx certificate.
If you are replacing an existing valid certificate and want certbot to request a new one immediately, pass `force=true`:
```bash
make run_tls host=example.com email=admin@example.com staging=false force=true
```
The generated certificate paths in `docker/.env` are:
```env
MG_NGINX_SERVER_CERT=./ssl/letsencrypt/live/<host>/fullchain.pem
MG_NGINX_SERVER_KEY=./ssl/letsencrypt/live/<host>/privkey.pem
```
The setup script comments or uncomments those values automatically. Operators should not need to edit them by hand.
The certbot service keeps running and checks renewal twice a day. When a certificate is renewed, it sends a `HUP` signal to the Nginx process so new TLS handshakes use the renewed certificate.
## Makefile Integration
The included `Makefile` defines build and Dockerbuild targets for all Magistrala services. Key points:
+59
View File
@@ -525,6 +525,8 @@ services:
- type: bind
source: ${MG_NGINX_SERVER_DHPARAM:-./ssl/dhparam.pem}
target: /etc/ssl/certs/dhparam.pem
- ./ssl/letsencrypt:/etc/letsencrypt:ro
- ./ssl/certbot-www:/var/www/certbot:ro
ports:
- ${MG_NGINX_HTTP_PORT}:${MG_NGINX_HTTP_PORT}
- ${MG_NGINX_SSL_PORT}:${MG_NGINX_SSL_PORT}
@@ -544,6 +546,63 @@ services:
soft: 65536
hard: 65536
certbot:
image: docker.io/certbot/certbot:v2.11.0
container_name: magistrala-certbot
profiles:
- letsencrypt
depends_on:
- nginx
pid: "service:nginx"
restart: unless-stopped
env_file:
- .env
volumes:
- ./ssl/letsencrypt:/etc/letsencrypt
- ./ssl/certbot-www:/var/www/certbot
entrypoint: /bin/sh
command:
- -c
- |
if [ -z "$${MG_PUBLIC_HOST}" ] || [ "$${MG_PUBLIC_HOST}" = "localhost" ]; then
echo "Set MG_PUBLIC_HOST to a public DNS name before requesting a Let's Encrypt certificate." >&2
exit 1
fi
if [ -z "$${MG_LETSENCRYPT_EMAIL}" ]; then
echo "Set MG_LETSENCRYPT_EMAIL before requesting a Let's Encrypt certificate." >&2
exit 1
fi
staging_arg=""
if [ "$${MG_LETSENCRYPT_STAGING}" = "true" ]; then
staging_arg="--staging"
fi
renewal_arg="--keep-until-expiring"
if [ "$${MG_LETSENCRYPT_FORCE_RENEWAL}" = "true" ]; then
renewal_arg="--force-renewal"
fi
certbot certonly \
--webroot \
--webroot-path /var/www/certbot \
--domain "$${MG_PUBLIC_HOST}" \
--email "$${MG_LETSENCRYPT_EMAIL}" \
--agree-tos \
--no-eff-email \
--non-interactive \
$${renewal_arg} \
$${staging_arg}
while :; do
certbot renew \
--webroot \
--webroot-path /var/www/certbot \
--quiet \
--deploy-hook "kill -HUP 1" \
$${staging_arg}
sleep 12h & wait $$!
done
clients-db:
image: docker.io/postgres:18.0-alpine3.22
container_name: magistrala-clients-db
+9
View File
@@ -71,6 +71,12 @@ http {
add_header Access-Control-Allow-Methods '*';
add_header Access-Control-Allow-Headers '*';
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
try_files $uri =404;
}
# Proxy pass to auth service
location ~ ^/(pats) {
include snippets/proxy-headers.conf;
@@ -156,6 +162,9 @@ http {
include snippets/ws-upgrade.conf;
proxy_pass http://mqtt_ws_cluster;
}
# UI proxy populated by docker/setup-tls.sh; empty = no catch-all (local dev)
include snippets/ui-proxy.conf;
}
}
+9
View File
@@ -80,6 +80,12 @@ http {
add_header Access-Control-Allow-Methods '*';
add_header Access-Control-Allow-Headers '*';
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
try_files $uri =404;
}
# Proxy pass to auth service
location ~ ^/(pats) {
include snippets/proxy-headers.conf;
@@ -168,6 +174,9 @@ http {
include snippets/ws-upgrade.conf;
proxy_pass http://mqtt_ws_cluster;
}
# UI proxy populated by docker/setup-tls.sh; empty = no catch-all (local dev)
include snippets/ui-proxy.conf;
}
}
+2
View File
@@ -3,6 +3,8 @@
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+5
View File
@@ -0,0 +1,5 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
#
# This file is written by docker/setup-tls.sh for TLS deployments.
# It is intentionally empty for local development (make run_latest).
+242
View File
@@ -0,0 +1,242 @@
#!/usr/bin/env sh
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
ENV_FILE="$ROOT_DIR/docker/.env"
COMPOSE_FILE="$ROOT_DIR/docker/docker-compose.yaml"
HOST=${MG_PUBLIC_HOST:-}
EMAIL=${MG_LETSENCRYPT_EMAIL:-}
LETSENCRYPT_ENABLED=${MG_LETSENCRYPT_ENABLED:-true}
STAGING=${MG_LETSENCRYPT_STAGING:-true}
FORCE_RENEWAL=${MG_LETSENCRYPT_FORCE_RENEWAL:-false}
PROJECT=${DOCKER_PROJECT:-magistrala}
TIMEOUT_SECONDS=${MG_LETSENCRYPT_TIMEOUT_SECONDS:-180}
usage() {
cat <<EOF
Usage:
MG_PUBLIC_HOST=example.com MG_LETSENCRYPT_EMAIL=admin@example.com [MG_LETSENCRYPT_STAGING=false] $0
MG_PUBLIC_HOST=example.com MG_LETSENCRYPT_ENABLED=false $0
Required:
MG_PUBLIC_HOST Public DNS name that points to this Docker host.
MG_LETSENCRYPT_EMAIL Email address for Let's Encrypt notices when
MG_LETSENCRYPT_ENABLED=true.
Optional:
MG_LETSENCRYPT_ENABLED true by default. Set false to use the fallback
Nginx certificate and comment out Let's Encrypt
cert/key paths in docker/.env.
MG_LETSENCRYPT_STAGING true by default. Set false for production certs.
MG_LETSENCRYPT_FORCE_RENEWAL
false by default. Set true to replace an existing cert.
DOCKER_PROJECT Compose project name. Defaults to magistrala.
MG_LETSENCRYPT_TIMEOUT_SECONDS
Wait time for certificate files. Defaults to 180.
EOF
}
if [ -z "$HOST" ]; then
usage >&2
exit 2
fi
case "$LETSENCRYPT_ENABLED" in
true|false)
;;
*)
echo "MG_LETSENCRYPT_ENABLED must be true or false." >&2
exit 2
;;
esac
if [ "$LETSENCRYPT_ENABLED" = "true" ] && [ -z "$EMAIL" ]; then
usage >&2
exit 2
fi
if [ "$LETSENCRYPT_ENABLED" = "true" ] && [ "$HOST" = "localhost" ]; then
echo "MG_PUBLIC_HOST must be a public DNS name, not localhost." >&2
exit 2
fi
if [ ! -f "$ENV_FILE" ]; then
echo "Missing $ENV_FILE" >&2
exit 1
fi
set_env() {
key=$1
value=$2
tmp=$(mktemp)
awk -v key="$key" -v value="$value" '
BEGIN { done = 0 }
!done && (index($0, key "=") == 1 || index($0, "#" key "=") == 1) {
print key "=" value
done = 1
next
}
{ print }
END {
if (!done) {
print key "=" value
}
}
' "$ENV_FILE" > "$tmp"
mv "$tmp" "$ENV_FILE"
}
comment_env() {
key=$1
value=$2
tmp=$(mktemp)
awk -v key="$key" -v value="$value" '
BEGIN { done = 0 }
!done && (index($0, key "=") == 1 || index($0, "# " key "=") == 1 || index($0, "#" key "=") == 1) {
print "# " key "=" value
done = 1
next
}
{ print }
END {
if (!done) {
print "# " key "=" value
}
}
' "$ENV_FILE" > "$tmp"
mv "$tmp" "$ENV_FILE"
}
compose() {
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT" "$@"
}
write_ui_proxy() {
ui_host=${MG_UI_HOST:-ui}
cat > "$ROOT_DIR/docker/nginx/snippets/ui-proxy.conf" <<NGINX
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
# Written by docker/setup-tls.sh do not edit manually.
location / {
if (\$scheme = http) {
return 301 https://\$host\$request_uri;
}
include snippets/proxy-headers.conf;
include snippets/ws-upgrade.conf;
proxy_pass http://${ui_host}:3000;
}
NGINX
}
wait_for_nginx_http() {
if ! command -v curl >/dev/null 2>&1; then
return 0
fi
elapsed=0
while [ "$elapsed" -lt "$TIMEOUT_SECONDS" ]; do
status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 \
http://127.0.0.1/.well-known/acme-challenge/magistrala-tls-probe 2>/dev/null || true)
case "$status" in
200|301|302|307|308|404)
return 0
;;
esac
sleep 2
elapsed=$((elapsed + 2))
done
echo "Timed out waiting for Nginx to accept HTTP traffic." >&2
docker logs --tail 80 magistrala-nginx >&2 || true
exit 1
}
cert_path="./ssl/letsencrypt/live/$HOST/fullchain.pem"
key_path="./ssl/letsencrypt/live/$HOST/privkey.pem"
cert_file="$ROOT_DIR/docker/ssl/letsencrypt/live/$HOST/fullchain.pem"
key_file="$ROOT_DIR/docker/ssl/letsencrypt/live/$HOST/privkey.pem"
if [ "$LETSENCRYPT_ENABLED" = "false" ]; then
FORCE_RENEWAL=false
fi
if [ "$LETSENCRYPT_ENABLED" = "true" ] && [ "$STAGING" = "false" ] && [ -f "$cert_file" ]; then
if openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null | grep -q "STAGING"; then
FORCE_RENEWAL=true
fi
fi
echo "Configuring docker/.env for $HOST"
set_env MG_RELEASE_TAG latest
set_env MG_PUBLIC_HOST "$HOST"
set_env MG_UI_HOST "${MG_UI_HOST:-ui}"
set_env MG_LETSENCRYPT_ENABLED "$LETSENCRYPT_ENABLED"
set_env MG_LETSENCRYPT_EMAIL "$EMAIL"
set_env MG_LETSENCRYPT_STAGING "$STAGING"
set_env MG_LETSENCRYPT_FORCE_RENEWAL "$FORCE_RENEWAL"
set_env MG_NGINX_SERVER_NAME "$HOST"
comment_env MG_NGINX_SERVER_CERT "$cert_path"
comment_env MG_NGINX_SERVER_KEY "$key_path"
set_env MG_UI_DOCKER_ACCEPT_EULA yes
set_env MG_OAUTH_UI_REDIRECT_URL "https://$HOST/api/auth/token"
set_env MG_OAUTH_UI_ERROR_URL "https://$HOST/login"
set_env MG_PASSWORD_RESET_URL_PREFIX "https://$HOST/password-reset"
set_env MG_VERIFICATION_URL_PREFIX "https://$HOST/verify-email"
set_env MG_GOOGLE_REDIRECT_URL "https://$HOST/oauth/callback/google"
set_env NEXTAUTH_URL "https://$HOST"
set_env MG_HOST_URL "https://$HOST"
set_env MG_UI_BASEURL "https://$HOST"
set_env MG_UI_CLI_MQTT_HOST "$HOST"
set_env MG_UI_CLI_WS_URL "wss://$HOST/mqtt"
set_env MG_UI_CLI_COAP_HOST "$HOST"
set_env MG_UI_CLI_HTTP_URL "https://$HOST/http"
mkdir -p "$ROOT_DIR/docker/ssl/letsencrypt" "$ROOT_DIR/docker/ssl/certbot-www"
write_ui_proxy
if [ "$LETSENCRYPT_ENABLED" = "false" ]; then
echo "Starting Magistrala with the fallback Nginx certificate"
MG_UI_DOCKER_ACCEPT_EULA=yes compose up -d
MG_UI_DOCKER_ACCEPT_EULA=yes COMPOSE_PROFILES=letsencrypt compose stop certbot >/dev/null 2>&1 || true
echo "Let's Encrypt disabled. Nginx cert/key paths are commented in docker/.env."
echo "Fallback TLS setup complete: https://$HOST/"
exit 0
fi
echo "Starting Magistrala with the fallback Nginx certificate"
MG_UI_DOCKER_ACCEPT_EULA=yes compose up -d
wait_for_nginx_http
echo "Requesting Let's Encrypt certificate for $HOST"
MG_UI_DOCKER_ACCEPT_EULA=yes COMPOSE_PROFILES=letsencrypt compose up -d --force-recreate certbot
elapsed=0
while [ "$elapsed" -lt "$TIMEOUT_SECONDS" ]; do
if [ -s "$cert_file" ] && [ -s "$key_file" ]; then
break
fi
sleep 2
elapsed=$((elapsed + 2))
done
if [ ! -s "$cert_file" ] || [ ! -s "$key_file" ]; then
echo "Timed out waiting for Let's Encrypt certificate files." >&2
docker logs --tail 80 magistrala-certbot >&2 || true
exit 1
fi
echo "Switching Nginx to the issued certificate"
set_env MG_NGINX_SERVER_CERT "$cert_path"
set_env MG_NGINX_SERVER_KEY "$key_path"
set_env MG_LETSENCRYPT_FORCE_RENEWAL false
MG_UI_DOCKER_ACCEPT_EULA=yes compose up -d --force-recreate nginx
echo "TLS setup complete: https://$HOST/"
+2
View File
@@ -7,3 +7,5 @@
*conf
client.crt
client.key
certbot-www/
letsencrypt/