#!/bin/bash set -e test_ca_list=() case "$1" in --test) ln -sf test.env .env test_ca_list=(test/certs/cur-root.crt test/certs/pebble.minica.pem) ;; --production) ln -sf production.env .env ;; *) echo "usage: $0 --test|--production" >&2 exit 1 ;; esac . .env subdomains=('' mail git forum) function fatal() { echo "fatal: $*" >&2 exit 1 } function random_passwd() { tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 16 } function wait_for_server() { local i echo "waiting for server to start up..." for i in {1..30}; do sleep 1 if curl "http://$BASE_DOMAIN_NAME/" >/dev/null; then echo "server up" return fi done fatal "server failed to start up after $i attempts" } function retry_if_failed() { local retry error_args=": $*" if [[ "$1" == "-q" ]]; then error_args="" shift fi for retry in {1..3}; do if "$@"; then return fi echo "command failed (retry $retry)$error_args" >&2 sleep 2 done fatal "command failed$error_args" } function forgejo() { docker run --rm codeberg.org/forgejo/forgejo:7 forgejo "$@" } function write_config() { local src="" dest="" vars="" new_vars=() mode="644" owner="root:root" while (($#)); do case "$1" in --src) src="$2" shift 2 ;; --mode) mode="$2" shift 2 ;; --owner) owner="$2" shift 2 ;; --dest) dest="$2" shift 2 ;; --var) [[ "$2" =~ ^([A-Za-z0-9_]+)= ]] || fatal "invalid --var argument" vars+="\$${BASH_REMATCH[1]} " new_vars+=("$2") shift 2 ;; *) fatal "write_config: unrecognized argument: $1" ;; esac done : "${src:?missing --src argument}" local dest_dir temp dest_dir="$(dirname "${dest:?missing --dest argument}")" temp="$(umask 577 && mktemp -p "$dest_dir")" # printf '%q ' env "${new_vars[@]}" envsubst "$vars"; # echo "<" "$src" ">" "$temp" env "${new_vars[@]}" envsubst "$vars" < "$src" > "$temp" || { rm -f "$temp"; exit 1; } chmod "$mode" "$temp" || { rm -f "$temp"; exit 1; } chown "$owner" "$temp" || { rm -f "$temp"; exit 1; } if [[ ! -f "$dest" ]] && mv -v -T "$temp" "$dest"; then return 0 fi if diff -u --label="expanded $src" "$temp" "$dest"; then rm -f "$temp" return 0 else rm -f "$temp" fatal "config file doesn't match generated config for $dest expanded from $src" fi } if [[ "$(id -u)" != 0 ]]; then fatal "must be ran as root" fi apt-get remove -y -q docker.io docker-doc docker-compose podman-docker containerd runc mkdir -p /var/lib/stalwart-mail apt-get update -y -q apt-get install ca-certificates curl jq gettext-base diffutils -y -q # force using overlay2 driver so btrfs snapshots will snapshot the entire system and not miss all the docker stuff mkdir -p /etc/docker write_config --src templates/etc/docker/daemon.json --dest /etc/docker/daemon.json write_config --src templates/etc/apt/sources.list.d/docker.list \ --dest /etc/apt/sources.list.d/docker.list \ --var dpkg_arch="$(dpkg --print-architecture)" \ --var VERSION_CODENAME="$(. /etc/os-release && echo "$VERSION_CODENAME")" curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc apt-get update -y -q apt-get install certbot docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo openssl crudini git ssl-cert -y -q if ((${#test_ca_list[@]})); then install -m 644 "${test_ca_list[0]}" /usr/local/share/ca-certificates/test-root.crt install -m 644 "${test_ca_list[1]}" /usr/local/share/ca-certificates/test-root2.crt update-ca-certificates fi addgroup --gid=1000 git || true [[ "$(getent group 1000)" =~ ^'git:x:1000:' ]] || fatal "some other group has gid 1000, which is needed for the git group" adduser --system --shell=/bin/bash --gecos 'Git Version Control' --uid=1000 --ingroup=git --disabled-password --home=/var/lib/forgejo git || true [[ "$(getent passwd 1000)" == 'git:x:1000:1000:Git Version Control,,,:/var/lib/forgejo:/bin/bash' ]] || fatal "some other user has gid 1000, which is needed for the git user" [[ -f ~git/.ssh/id_ed25519 ]] || sudo -u git ssh-keygen -f ~git/.ssh/id_ed25519 -t ed25519 -C "Forgejo Host Key" -N "" [[ -f ~git/.ssh/authorized_keys ]] || sudo -u git cat ~git/.ssh/id_ed25519.pub | sudo -u git tee ~git/.ssh/authorized_keys sudo -u git chmod 600 ~git/.ssh/authorized_keys write_config --src templates/usr/local/bin/gitea --dest /usr/local/bin/gitea --mode 755 mkdir -p /etc/forgejo rm -rf /var/www/html mkdir -p /var/www/html chown git:git /var/www/html chmod 775 /var/www/html (cd /var/www/html && sudo -u git git init --no-initial-branch) (cd /var/www/html && sudo -u git git remote add -t heads/rendered --mirror=fetch origin /data/git/repositories/libre-chip/website.git) chown root:git /etc/forgejo chmod 770 /etc/forgejo if [[ ! -f /etc/forgejo/app.ini ]]; then write_config --src templates/etc/forgejo/app.ini \ --dest /etc/forgejo/app.ini --mode 640 --owner root:git \ --var BASE_DOMAIN_NAME="$BASE_DOMAIN_NAME" \ --var SECRET_KEY="$(forgejo generate secret SECRET_KEY)" \ --var INTERNAL_TOKEN="$(forgejo generate secret INTERNAL_TOKEN)" \ --var MAIL_PASSWD="$(random_passwd)" \ --var JWT_SECRET="$(forgejo generate secret JWT_SECRET)" \ --var LFS_JWT_SECRET="$(forgejo generate secret LFS_JWT_SECRET)" fi mkdir -p /var/lib/stalwart-mail/etc mail_passwd="" mail_passwd_hash="" if [[ ! -f /var/lib/stalwart-mail/etc/config.toml ]]; then mail_passwd="$(random_passwd)" write_config --src templates/var/lib/stalwart-mail/cli.sh \ --dest /var/lib/stalwart-mail/cli.sh --mode 400 \ --var wd="$(pwd)" --var mail_passwd="$mail_passwd" mail_passwd_hash="$(echo -n "$mail_passwd" | openssl passwd -6 -stdin)" write_config --src templates/var/lib/stalwart-mail/etc/config.toml \ --dest /var/lib/stalwart-mail/etc/config.toml --mode 600 \ --var mail_passwd_hash="$mail_passwd_hash" \ --var BASE_DOMAIN_NAME="$BASE_DOMAIN_NAME" fi . /var/lib/stalwart-mail/cli.sh if [[ ! -f /var/discourse/containers/app.yml ]]; then if [[ ! -d /var/discourse/containers ]]; then git clone https://github.com/discourse/discourse_docker.git /var/discourse chmod 700 /var/discourse/containers fi forum_smtp_passwd="$(random_passwd)" write_config --src templates/var/discourse/containers/app.yml \ --dest /var/discourse/containers/app.yml --mode 400 \ --var BASE_DOMAIN_NAME="$BASE_DOMAIN_NAME" \ --var forum_smtp_passwd="$forum_smtp_passwd" \ --var mail_passwd="$mail_passwd" fi wd="$(pwd)" if ! [[ "$wd" =~ ^/[-/a-zA-Z0-9_]*$ ]]; then fatal "invalid characters in current directory: $wd" fi nginx_container="$(docker create --rm -v /var/www/.well-known/acme-challenge:/var/www/.well-known/acme-challenge:ro -v "$wd"/http_only_nginx_templates:/etc/nginx/templates:ro -p 80:80 nginx:bookworm)" docker start "$nginx_container" trap 'docker stop "$nginx_container"' EXIT echo "waiting for server to come up..." for _ in {0..30}; do sleep 1 if curl "http://$BASE_DOMAIN_NAME/" >/dev/null; then break fi done echo "server up" certbot_args=(certonly -n --email "postmaster@$BASE_DOMAIN_NAME" "--server=$ACME_SERVER_URL" --cert-name server --agree-tos --webroot --webroot-path /var/www) certbot_args+=(--disable-hook-validation --post-hook "cd '$wd' && docker compose -p server restart") for subdomain in "${subdomains[@]}"; do if [[ -n "$subdomain" ]]; then subdomain+=. fi certbot_args+=(-d "$subdomain$BASE_DOMAIN_NAME") certbot_args+=(-d "$subdomain$ALT_BASE_DOMAIN_NAME") done retry_if_failed certbot "${certbot_args[@]}" trap EXIT docker stop "$nginx_container" DOCKER_BUILDKIT=1 docker compose -p server up -d sleep 10 if [[ -n "$mail_passwd_hash" ]]; then forgejo_smtp_passwd="$(crudini --get /etc/forgejo/app.ini mailer PASSWD)" stalwart-cli domain create "$BASE_DOMAIN_NAME" curl -u "admin:$mail_passwd" "https://mail.$BASE_DOMAIN_NAME/api/dkim" --data-binary '{"id":null,"algorithm":"Ed25519","domain":"'"$BASE_DOMAIN_NAME"'","selector":null}' > /dev/null curl -u "admin:$mail_passwd" "https://mail.$BASE_DOMAIN_NAME/api/dkim" --data-binary '{"id":null,"algorithm":"Rsa","domain":"'"$BASE_DOMAIN_NAME"'","selector":null}' > /dev/null stalwart-cli account create -d 'Admin Account' -i true -a "postmaster@$BASE_DOMAIN_NAME" 'admin' "$mail_passwd" stalwart-cli account create -d 'Forgejo Server' -i false -a "forgejo@$BASE_DOMAIN_NAME" 'forgejo' "$forgejo_smtp_passwd" add_postmaster=(docker compose -p server exec -T -u git forgejo forgejo admin user create --admin --username postmaster --password "$mail_passwd" --email "postmaster@$BASE_DOMAIN_NAME") retry_if_failed -q "${add_postmaster[@]}" forum_smtp_passwd="$(sed 's/^ *DISCOURSE_SMTP_PASSWORD: "*\([^"]*\)"$/\1/p; d' < /var/discourse/containers/app.yml)" [[ -n "$forum_smtp_passwd" ]] || fatal "can't parse smtp password out of /var/discourse/containers/app.yml" stalwart-cli account create -d 'Forum Replies' -i false -a "@$BASE_DOMAIN_NAME" 'forum' "$forum_smtp_passwd" stalwart-cli account create -d 'Forum Notifications' -i false -a "forum-noreply@$BASE_DOMAIN_NAME" 'forum-noreply' "$forum_smtp_passwd" forgejo_api=(retry_if_failed -q curl --fail-with-body -u "postmaster:$mail_passwd" -H 'Accept: application/json' -H 'Content-Type: application/json') "${forgejo_api[@]}" -X 'POST' "https://git.$BASE_DOMAIN_NAME/api/v1/orgs" -d '{"username": "libre-chip"}' > /dev/null "${forgejo_api[@]}" -X 'POST' "https://git.$BASE_DOMAIN_NAME/api/v1/orgs/libre-chip/repos" -d '{"name": "website"}' > /dev/null post_receive_hook="$(jq -csR '{content:.}' < website_git_post_receive_hook.sh)" "${forgejo_api[@]}" -X 'PATCH' "https://git.$BASE_DOMAIN_NAME/api/v1/repos/libre-chip/website/hooks/git/post-receive" -d "$post_receive_hook" > /dev/null fi ( cd /var/discourse # must run after starting mail server since it validates POP3 ./launcher bootstrap app ./launcher start app )