Initial commit
This commit is contained in:
@@ -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"
|
||||
@@ -0,0 +1,20 @@
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server';
|
||||
|
||||
const mongo = await MongoMemoryServer.create({
|
||||
instance: {
|
||||
port: 27017,
|
||||
dbName: 'lean101',
|
||||
ip: '127.0.0.1'
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`MongoDB demo server running at ${mongo.getUri()}`);
|
||||
console.log('Press Ctrl+C to stop it.');
|
||||
|
||||
const shutdown = async () => {
|
||||
await mongo.stop();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
Reference in New Issue
Block a user