245 lines
5.4 KiB
Bash
245 lines
5.4 KiB
Bash
#!/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"
|