Initial commit

This commit is contained in:
2026-05-06 22:38:06 +12:00
commit 0342b5fb9d
54 changed files with 22513 additions and 0 deletions
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
sudo ./scripts/deploy-do.sh --domain example.com [options]
Required:
--domain DOMAIN Primary domain name, for example lean-101.com
Optional:
--email EMAIL Email for Let's Encrypt. If omitted, TLS is skipped.
--app-dir PATH Project directory on the server.
Default: current repo root
--app-port PORT Local Docker port exposed by compose.
Default: 8080
--with-www Also configure and request TLS for www.DOMAIN
--skip-certbot Skip Let's Encrypt even if --email is provided
--help Show this help
Examples:
sudo ./scripts/deploy-do.sh --domain lean-101.com --email ops@example.com --with-www
sudo ./scripts/deploy-do.sh --domain lean-101.com --skip-certbot
EOF
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
echo "Run this script with sudo or as root."
exit 1
fi
}
require_command() {
local command_name="$1"
if ! command -v "${command_name}" >/dev/null 2>&1; then
echo "Missing required command: ${command_name}"
exit 1
fi
}
pick_compose_package() {
if apt-cache show docker-compose-plugin >/dev/null 2>&1; then
echo "docker-compose-plugin"
return
fi
if apt-cache show docker-compose-v2 >/dev/null 2>&1; then
echo "docker-compose-v2"
return
fi
echo ""
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DOMAIN=""
EMAIL=""
APP_DIR="${REPO_ROOT}"
APP_PORT="8080"
WITH_WWW="false"
SKIP_CERTBOT="false"
while [[ $# -gt 0 ]]; do
case "$1" in
--domain)
DOMAIN="${2:-}"
shift 2
;;
--email)
EMAIL="${2:-}"
shift 2
;;
--app-dir)
APP_DIR="${2:-}"
shift 2
;;
--app-port)
APP_PORT="${2:-}"
shift 2
;;
--with-www)
WITH_WWW="true"
shift
;;
--skip-certbot)
SKIP_CERTBOT="true"
shift
;;
--help|-h)
usage
exit 0
;;
*)
echo "Unknown argument: $1"
usage
exit 1
;;
esac
done
if [[ -z "${DOMAIN}" ]]; then
echo "--domain is required."
usage
exit 1
fi
require_root
require_command apt-get
if [[ ! -f "${APP_DIR}/docker-compose.yml" ]]; then
echo "Could not find docker-compose.yml in ${APP_DIR}"
exit 1
fi
if [[ ! -f "${APP_DIR}/Dockerfile" ]]; then
echo "Could not find Dockerfile in ${APP_DIR}"
exit 1
fi
APT_PACKAGES=(
software-properties-common
docker.io
nginx
certbot
python3-certbot-nginx
)
echo "Installing server packages..."
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common
add-apt-repository -y universe
apt-get update
COMPOSE_PACKAGE="$(pick_compose_package)"
if [[ -z "${COMPOSE_PACKAGE}" ]]; then
echo "Could not find a Docker Compose package via apt."
echo "Expected either docker-compose-plugin or docker-compose-v2."
exit 1
fi
APT_PACKAGES+=("${COMPOSE_PACKAGE}")
DEBIAN_FRONTEND=noninteractive apt-get install -y "${APT_PACKAGES[@]}"
echo "Enabling services..."
systemctl enable --now docker
systemctl enable --now nginx
if command -v ufw >/dev/null 2>&1; then
echo "Configuring UFW rules..."
ufw allow OpenSSH >/dev/null 2>&1 || true
ufw allow 'Nginx Full' >/dev/null 2>&1 || true
fi
PUBLIC_SITE_URL="https://${DOMAIN}"
echo "Writing ${APP_DIR}/.env..."
cat > "${APP_DIR}/.env" <<EOF
PUBLIC_SITE_URL=${PUBLIC_SITE_URL}
EOF
echo "Writing ${APP_DIR}/docker-compose.override.yml..."
cat > "${APP_DIR}/docker-compose.override.yml" <<EOF
services:
lean101:
ports:
- "127.0.0.1:${APP_PORT}:3000"
EOF
echo "Building and starting the Docker stack..."
(
cd "${APP_DIR}"
docker compose up -d --build
)
NGINX_SITE="/etc/nginx/sites-available/${DOMAIN}"
NGINX_LINK="/etc/nginx/sites-enabled/${DOMAIN}"
SERVER_ALIASES="${DOMAIN}"
CERTBOT_DOMAINS=(-d "${DOMAIN}")
if [[ "${WITH_WWW}" == "true" ]]; then
SERVER_ALIASES="${DOMAIN} www.${DOMAIN}"
CERTBOT_DOMAINS+=(-d "www.${DOMAIN}")
fi
echo "Writing nginx site config to ${NGINX_SITE}..."
cat > "${NGINX_SITE}" <<EOF
server {
listen 80;
listen [::]:80;
server_name ${SERVER_ALIASES};
location / {
proxy_pass http://127.0.0.1:${APP_PORT};
proxy_http_version 1.1;
proxy_set_header Host \$host;
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;
}
}
EOF
if [[ -L /etc/nginx/sites-enabled/default ]]; then
rm -f /etc/nginx/sites-enabled/default
fi
ln -sf "${NGINX_SITE}" "${NGINX_LINK}"
echo "Validating nginx config..."
nginx -t
systemctl reload nginx
if [[ "${SKIP_CERTBOT}" == "false" && -n "${EMAIL}" ]]; then
echo "Requesting Let's Encrypt certificate..."
certbot --nginx \
--non-interactive \
--agree-tos \
--redirect \
-m "${EMAIL}" \
"${CERTBOT_DOMAINS[@]}"
nginx -t
systemctl reload nginx
else
echo "Skipping certbot. The site is currently configured for HTTP only."
fi
echo
echo "Deployment complete."
echo "Project directory: ${APP_DIR}"
echo "Domain: ${DOMAIN}"
echo "Public URL: ${PUBLIC_SITE_URL}"
echo "Container upstream: http://127.0.0.1:${APP_PORT}"
echo
echo "Useful commands:"
echo " cd ${APP_DIR} && docker compose ps"
echo " cd ${APP_DIR} && docker compose logs -f"
echo " systemctl status nginx"