This commit is contained in:
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
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_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
+1
View File
@@ -2,3 +2,4 @@ services:
nginx:
volumes:
- ./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"
volumes:
- ./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
environment:
BROKER_DOMAIN: ${BROKER_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:
- no-new-privileges:true
+2
View File
@@ -11,6 +11,8 @@
!gitea/config/.gitkeep
!letsencrypt/
!letsencrypt/.gitkeep
!ssl/
!ssl/.gitkeep
!certbot-webroot/
!certbot-webroot/.gitkeep
!backups/
+1
View File
@@ -0,0 +1 @@
+4 -4
View File
@@ -28,8 +28,8 @@ server {
http2 on;
server_name ${BROKER_DOMAIN};
ssl_certificate /etc/letsencrypt/live/${BROKER_DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${BROKER_DOMAIN}/privkey.pem;
ssl_certificate /etc/nginx/ssl/${SSL_CERTIFICATE_FILE};
ssl_certificate_key /etc/nginx/ssl/${SSL_CERTIFICATE_KEY_FILE};
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_protocols TLSv1.2 TLSv1.3;
@@ -56,8 +56,8 @@ server {
http2 on;
server_name ${GITEA_DOMAIN};
ssl_certificate /etc/letsencrypt/live/${BROKER_DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${BROKER_DOMAIN}/privkey.pem;
ssl_certificate /etc/nginx/ssl/${SSL_CERTIFICATE_FILE};
ssl_certificate_key /etc/nginx/ssl/${SSL_CERTIFICATE_KEY_FILE};
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_protocols TLSv1.2 TLSv1.3;
+26 -6
View File
@@ -57,6 +57,7 @@ config/
│ ├── config/ # app.ini and Gitea configuration
│ └── data/ # repositories, packages, and gitea.db
├── letsencrypt/ # account data, certificate, and private key
├── ssl/ # optional administrator-provided certificate
├── certbot-webroot/ # HTTP-01 challenge files
└── 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,
starts Gitea and the HTTP-only Nginx configuration, obtains one certificate
covering both domains, and switches Nginx to TLS.
starts Gitea and the HTTP-only Nginx configuration, obtains a certificate when
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:
@@ -122,7 +139,7 @@ the transition.
## 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
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
cd /YOUR/INSTALL/PARENT/TA-Deployment-Access
git pull --ff-only
docker compose --env-file .env -f deploy/gitea/compose.yaml pull
./manage.sh start
./update.sh
```
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.
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)"
GITEA_DOMAIN="$(read_env_setting GITEA_DOMAIN)"
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}"
: "${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() {
@@ -42,6 +65,7 @@ prepare() {
config/gitea/data \
config/gitea/config \
config/letsencrypt \
config/ssl \
config/certbot-webroot \
config/backups
chmod 700 config/broker config/gitea/data config/gitea/config config/backups
@@ -66,12 +90,14 @@ bootstrap)
prepare
gitea_compose up -d
docker compose up -d nginx
docker compose --profile tools run --rm certbot certonly \
--webroot --webroot-path /var/www/certbot \
--non-interactive --agree-tos \
--email "$LETSENCRYPT_EMAIL" \
--cert-name "$BROKER_DOMAIN" \
-d "$BROKER_DOMAIN" -d "$GITEA_DOMAIN"
if [[ "$SSL_MODE" == "letsencrypt" ]]; then
docker compose --profile tools run --rm certbot certonly \
--webroot --webroot-path /var/www/certbot \
--non-interactive --agree-tos \
--email "$LETSENCRYPT_EMAIL" \
--cert-name "$BROKER_DOMAIN" \
-d "$BROKER_DOMAIN" -d "$GITEA_DOMAIN"
fi
docker compose -f compose.yaml -f compose.tls.yaml up -d nginx
;;
start)
@@ -82,6 +108,8 @@ start)
;;
renew)
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 -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"