This commit is contained in:
David Schroeder
2026-07-26 14:15:24 -05:00
parent ed61527cf0
commit 4511d3a0a3
9 changed files with 169 additions and 18 deletions
+7
View File
@@ -5,6 +5,13 @@ HTTP_PORT=8680
HTTPS_PORT=8643 HTTPS_PORT=8643
GITEA_SSH_PORT=2222 GITEA_SSH_PORT=2222
# letsencrypt: manage a certificate automatically with Certbot.
# custom: read the named certificate files from ./config/ssl.
SSL_MODE=letsencrypt
SSL_CERTIFICATE_DIR=./config/letsencrypt/live/broker.example.com
SSL_CERTIFICATE_FILE=fullchain.pem
SSL_CERTIFICATE_KEY_FILE=privkey.pem
TAPM_LISTEN_ADDR=:8080 TAPM_LISTEN_ADDR=:8080
TAPM_DATABASE_DSN=file:/data/tapm.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL) TAPM_DATABASE_DSN=file:/data/tapm.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)
TAPM_PUBLIC_URL=https://broker.example.com TAPM_PUBLIC_URL=https://broker.example.com
+1
View File
@@ -2,3 +2,4 @@ services:
nginx: nginx:
volumes: volumes:
- ./deploy/nginx/templates/tls.conf.template:/etc/nginx/templates/default.conf.template:ro - ./deploy/nginx/templates/tls.conf.template:/etc/nginx/templates/default.conf.template:ro
- ${SSL_CERTIFICATE_DIR:-./config/ssl}:/etc/nginx/ssl:ro
+2 -1
View File
@@ -37,11 +37,12 @@ services:
- "${HTTPS_PORT:-8643}:443" - "${HTTPS_PORT:-8643}:443"
volumes: volumes:
- ./deploy/nginx/templates/bootstrap.conf.template:/etc/nginx/templates/default.conf.template:ro - ./deploy/nginx/templates/bootstrap.conf.template:/etc/nginx/templates/default.conf.template:ro
- ./config/letsencrypt:/etc/letsencrypt:ro
- ./config/certbot-webroot:/var/www/certbot:ro - ./config/certbot-webroot:/var/www/certbot:ro
environment: environment:
BROKER_DOMAIN: ${BROKER_DOMAIN} BROKER_DOMAIN: ${BROKER_DOMAIN}
GITEA_DOMAIN: ${GITEA_DOMAIN} GITEA_DOMAIN: ${GITEA_DOMAIN}
SSL_CERTIFICATE_FILE: ${SSL_CERTIFICATE_FILE:-fullchain.pem}
SSL_CERTIFICATE_KEY_FILE: ${SSL_CERTIFICATE_KEY_FILE:-privkey.pem}
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true
+2
View File
@@ -11,6 +11,8 @@
!gitea/config/.gitkeep !gitea/config/.gitkeep
!letsencrypt/ !letsencrypt/
!letsencrypt/.gitkeep !letsencrypt/.gitkeep
!ssl/
!ssl/.gitkeep
!certbot-webroot/ !certbot-webroot/
!certbot-webroot/.gitkeep !certbot-webroot/.gitkeep
!backups/ !backups/
+1
View File
@@ -0,0 +1 @@
+4 -4
View File
@@ -28,8 +28,8 @@ server {
http2 on; http2 on;
server_name ${BROKER_DOMAIN}; server_name ${BROKER_DOMAIN};
ssl_certificate /etc/letsencrypt/live/${BROKER_DOMAIN}/fullchain.pem; ssl_certificate /etc/nginx/ssl/${SSL_CERTIFICATE_FILE};
ssl_certificate_key /etc/letsencrypt/live/${BROKER_DOMAIN}/privkey.pem; ssl_certificate_key /etc/nginx/ssl/${SSL_CERTIFICATE_KEY_FILE};
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d; ssl_session_timeout 1d;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
@@ -56,8 +56,8 @@ server {
http2 on; http2 on;
server_name ${GITEA_DOMAIN}; server_name ${GITEA_DOMAIN};
ssl_certificate /etc/letsencrypt/live/${BROKER_DOMAIN}/fullchain.pem; ssl_certificate /etc/nginx/ssl/${SSL_CERTIFICATE_FILE};
ssl_certificate_key /etc/letsencrypt/live/${BROKER_DOMAIN}/privkey.pem; ssl_certificate_key /etc/nginx/ssl/${SSL_CERTIFICATE_KEY_FILE};
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d; ssl_session_timeout 1d;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
+26 -6
View File
@@ -57,6 +57,7 @@ config/
│ ├── config/ # app.ini and Gitea configuration │ ├── config/ # app.ini and Gitea configuration
│ └── data/ # repositories, packages, and gitea.db │ └── data/ # repositories, packages, and gitea.db
├── letsencrypt/ # account data, certificate, and private key ├── letsencrypt/ # account data, certificate, and private key
├── ssl/ # optional administrator-provided certificate
├── certbot-webroot/ # HTTP-01 challenge files ├── certbot-webroot/ # HTTP-01 challenge files
└── backups/ # local offline snapshots └── backups/ # local offline snapshots
``` ```
@@ -70,8 +71,24 @@ The contents are ignored by Git. They must never be committed.
``` ```
This prepares the runtime directories, creates the private Docker network, This prepares the runtime directories, creates the private Docker network,
starts Gitea and the HTTP-only Nginx configuration, obtains one certificate starts Gitea and the HTTP-only Nginx configuration, obtains a certificate when
covering both domains, and switches Nginx to TLS. using Let's Encrypt, and switches Nginx to TLS.
For an administrator-provided certificate instead, place the certificate and
key in `config/ssl/` and set:
```dotenv
SSL_MODE=custom
SSL_CERTIFICATE_DIR=./config/ssl
SSL_CERTIFICATE_FILE=fullchain.pem
SSL_CERTIFICATE_KEY_FILE=privkey.pem
```
The certificate must cover both `BROKER_DOMAIN` and `GITEA_DOMAIN`. With custom
mode, `bootstrap` skips Certbot and `renew` is intentionally unavailable;
replace the files through the certificate provider's process and restart or
reload Nginx. `SSL_CERTIFICATE_DIR` can point at another directory relative to
the repository or at an absolute host path.
Create the initial Gitea administrator: Create the initial Gitea administrator:
@@ -122,7 +139,7 @@ the transition.
## 6. Renewal and backups ## 6. Renewal and backups
Run renewal twice daily from root's crontab: When `SSL_MODE=letsencrypt`, run renewal twice daily from root's crontab:
```cron ```cron
17 3,15 * * * cd /YOUR/INSTALL/PARENT/TA-Deployment-Access && ./manage.sh renew 17 3,15 * * * cd /YOUR/INSTALL/PARENT/TA-Deployment-Access && ./manage.sh renew
@@ -142,10 +159,13 @@ datacenter loss. Also back up `.env` through a secrets-aware system.
```sh ```sh
cd /YOUR/INSTALL/PARENT/TA-Deployment-Access cd /YOUR/INSTALL/PARENT/TA-Deployment-Access
git pull --ff-only ./update.sh
docker compose --env-file .env -f deploy/gitea/compose.yaml pull
./manage.sh start
``` ```
The updater operates only on the current VM. It fast-forwards the checked-out
branch, pulls the pinned Gitea image, rebuilds and restarts the local stack, and
waits for the local broker to become healthy. It has no peer discovery, SSH, or
multi-node update behavior.
Pin image versions as supplied and review release notes before changing them. Pin image versions as supplied and review release notes before changing them.
Never change Gitea between rootless and rootful image families in place. Never change Gitea between rootless and rootful image families in place.
+35 -7
View File
@@ -31,9 +31,32 @@ require_env() {
BROKER_DOMAIN="$(read_env_setting BROKER_DOMAIN)" BROKER_DOMAIN="$(read_env_setting BROKER_DOMAIN)"
GITEA_DOMAIN="$(read_env_setting GITEA_DOMAIN)" GITEA_DOMAIN="$(read_env_setting GITEA_DOMAIN)"
LETSENCRYPT_EMAIL="$(read_env_setting LETSENCRYPT_EMAIL)" LETSENCRYPT_EMAIL="$(read_env_setting LETSENCRYPT_EMAIL)"
SSL_MODE="$(read_env_setting SSL_MODE)"
SSL_MODE="${SSL_MODE:-letsencrypt}"
SSL_CERTIFICATE_DIR="$(read_env_setting SSL_CERTIFICATE_DIR)"
SSL_CERTIFICATE_FILE="$(read_env_setting SSL_CERTIFICATE_FILE)"
SSL_CERTIFICATE_KEY_FILE="$(read_env_setting SSL_CERTIFICATE_KEY_FILE)"
SSL_CERTIFICATE_FILE="${SSL_CERTIFICATE_FILE:-fullchain.pem}"
SSL_CERTIFICATE_KEY_FILE="${SSL_CERTIFICATE_KEY_FILE:-privkey.pem}"
: "${BROKER_DOMAIN:?BROKER_DOMAIN is required}" : "${BROKER_DOMAIN:?BROKER_DOMAIN is required}"
: "${GITEA_DOMAIN:?GITEA_DOMAIN is required}" : "${GITEA_DOMAIN:?GITEA_DOMAIN is required}"
: "${LETSENCRYPT_EMAIL:?LETSENCRYPT_EMAIL is required}" case "$SSL_MODE" in
letsencrypt)
: "${LETSENCRYPT_EMAIL:?LETSENCRYPT_EMAIL is required for SSL_MODE=letsencrypt}"
SSL_CERTIFICATE_DIR="${SSL_CERTIFICATE_DIR:-./config/letsencrypt/live/${BROKER_DOMAIN}}"
;;
custom)
SSL_CERTIFICATE_DIR="${SSL_CERTIFICATE_DIR:-./config/ssl}"
[[ -f "${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_FILE}" ]] ||
fail "custom certificate was not found at ${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_FILE}"
[[ -f "${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_KEY_FILE}" ]] ||
fail "custom certificate key was not found at ${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_KEY_FILE}"
;;
*)
fail "SSL_MODE must be letsencrypt or custom"
;;
esac
export SSL_CERTIFICATE_DIR SSL_CERTIFICATE_FILE SSL_CERTIFICATE_KEY_FILE
} }
prepare() { prepare() {
@@ -42,6 +65,7 @@ prepare() {
config/gitea/data \ config/gitea/data \
config/gitea/config \ config/gitea/config \
config/letsencrypt \ config/letsencrypt \
config/ssl \
config/certbot-webroot \ config/certbot-webroot \
config/backups config/backups
chmod 700 config/broker config/gitea/data config/gitea/config config/backups chmod 700 config/broker config/gitea/data config/gitea/config config/backups
@@ -66,12 +90,14 @@ bootstrap)
prepare prepare
gitea_compose up -d gitea_compose up -d
docker compose up -d nginx docker compose up -d nginx
docker compose --profile tools run --rm certbot certonly \ if [[ "$SSL_MODE" == "letsencrypt" ]]; then
--webroot --webroot-path /var/www/certbot \ docker compose --profile tools run --rm certbot certonly \
--non-interactive --agree-tos \ --webroot --webroot-path /var/www/certbot \
--email "$LETSENCRYPT_EMAIL" \ --non-interactive --agree-tos \
--cert-name "$BROKER_DOMAIN" \ --email "$LETSENCRYPT_EMAIL" \
-d "$BROKER_DOMAIN" -d "$GITEA_DOMAIN" --cert-name "$BROKER_DOMAIN" \
-d "$BROKER_DOMAIN" -d "$GITEA_DOMAIN"
fi
docker compose -f compose.yaml -f compose.tls.yaml up -d nginx docker compose -f compose.yaml -f compose.tls.yaml up -d nginx
;; ;;
start) start)
@@ -82,6 +108,8 @@ start)
;; ;;
renew) renew)
require_env require_env
[[ "$SSL_MODE" == "letsencrypt" ]] ||
fail "automatic renewal is only available with SSL_MODE=letsencrypt"
docker compose --profile tools run --rm certbot renew --quiet docker compose --profile tools run --rm certbot renew --quiet
docker compose -f compose.yaml -f compose.tls.yaml exec nginx nginx -s reload docker compose -f compose.yaml -f compose.tls.yaml exec nginx nginx -s reload
;; ;;
Executable
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -Eeuo pipefail
DEPLOY_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
SCRIPT_PATH="${DEPLOY_ROOT}/$(basename -- "${BASH_SOURCE[0]}")"
cd "$DEPLOY_ROOT"
fail() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
command -v git >/dev/null 2>&1 || fail "git is required"
command -v docker >/dev/null 2>&1 || fail "docker is required"
docker compose version >/dev/null 2>&1 ||
fail "Docker Compose v2 is required"
[[ -f .env ]] || fail ".env was not found in ${DEPLOY_ROOT}"
post_pull=0
if [[ "${1:-}" == "--post-pull" ]]; then
post_pull=1
elif [[ $# -ne 0 ]]; then
fail "usage: ${0} [--post-pull]"
fi
if ((post_pull == 0)); then
branch="$(git symbolic-ref --quiet --short HEAD)" ||
fail "the repository is in detached HEAD state"
upstream="$(git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null)" ||
fail "branch ${branch} does not have an upstream branch"
remote="$(git config --get "branch.${branch}.remote")" ||
fail "branch ${branch} does not have a configured remote"
git diff --quiet && git diff --cached --quiet ||
fail "tracked local changes must be committed or stashed before updating"
printf 'Checking %s against %s...\n' "$branch" "$upstream"
git fetch --quiet --prune "$remote"
local_commit="$(git rev-parse HEAD)"
upstream_commit="$(git rev-parse '@{upstream}')"
if [[ "$local_commit" == "$upstream_commit" ]]; then
printf 'Deployment is already up to date.\n'
elif git merge-base --is-ancestor "$local_commit" "$upstream_commit"; then
git merge --quiet --ff-only "$upstream_commit"
# Restart using the newly pulled copy so changes to this script apply.
exec "$SCRIPT_PATH" --post-pull
elif git merge-base --is-ancestor "$upstream_commit" "$local_commit"; then
fail "local branch ${branch} is ahead of ${upstream}; refusing to update"
else
fail "local branch ${branch} has diverged from ${upstream}"
fi
fi
printf 'Updating the local Gitea image and deployment stack...\n'
docker compose --env-file .env -f deploy/gitea/compose.yaml pull
"${DEPLOY_ROOT}/manage.sh" start
container_id="$(docker compose -f compose.yaml -f compose.tls.yaml ps -q broker)"
[[ -n "$container_id" ]] ||
fail "Docker Compose did not return the local broker container"
printf 'Waiting for the local broker health check'
for ((attempt = 1; attempt <= 75; attempt++)); do
status="$(
docker inspect \
--format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \
"$container_id"
)"
case "$status" in
healthy)
printf ' healthy.\n'
docker compose -f compose.yaml -f compose.tls.yaml ps
docker compose --env-file .env -f deploy/gitea/compose.yaml ps
printf 'Local deployment updated successfully.\n'
exit 0
;;
exited | dead | unhealthy)
printf ' %s.\n' "$status"
docker compose -f compose.yaml -f compose.tls.yaml logs --tail=100 broker
fail "the local broker failed its health check"
;;
esac
printf '.'
sleep 2
done
printf ' timed out.\n'
docker compose -f compose.yaml -f compose.tls.yaml logs --tail=100 broker
fail "the local broker did not become healthy within 150 seconds"