diff --git a/README.md b/README.md index f05ad9c..87fd2f2 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,16 @@ VS Code launch profiles in `.vscode/launch.json`: - `RpgRoller: Server + Edge (F5)` - `RpgRoller: Server + Firefox (F5)` +## Deployment + +Deploy to the Linux server with: + +```bash +bash ./scripts/deploy.sh +``` + +The script publishes the app locally, uploads a release to `/root/docker/rpgroller/releases/`, updates `/root/docker/rpgroller/current`, rebuilds the `rpgroller` image, and recreates the `rpgroller` container. The SQLite database is preserved because the container keeps using the existing bind mount at `/root/docker/rpgroller/data`. + Environment overrides: - Set `ConnectionStrings__RpgRoller` to point at a custom SQLite database. diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..f3a328c --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +readonly PROJECT_PATH="${REPO_ROOT}/RpgRoller/RpgRoller.csproj" + +readonly REMOTE_HOST="myvserver" +readonly REMOTE_ROOT="/root/docker/rpgroller" +readonly REMOTE_RELEASES_DIR="${REMOTE_ROOT}/releases" +readonly REMOTE_CURRENT_LINK="${REMOTE_ROOT}/current" +readonly REMOTE_DATA_DIR="${REMOTE_ROOT}/data" + +readonly CONTAINER_NAME="rpgroller" +readonly IMAGE_NAME="rpgroller" +readonly CONTAINER_PORT="8080" +readonly HOST_PORT="8082" +readonly RELEASE_TIMESTAMP="$(date -u +%Y%m%d%H%M%S)" +readonly LOCAL_STAGE_DIR="${REPO_ROOT}/artifacts/deploy/${RELEASE_TIMESTAMP}" +readonly LOCAL_PUBLISH_DIR="${LOCAL_STAGE_DIR}/publish" +readonly REMOTE_RELEASE_DIR="${REMOTE_RELEASES_DIR}/${RELEASE_TIMESTAMP}" + +cleanup() { + rm -rf "${LOCAL_STAGE_DIR}" +} + +trap cleanup EXIT + +require_tool() { + local tool_name="$1" + if ! command -v "${tool_name}" >/dev/null 2>&1; then + printf 'Required tool not found: %s\n' "${tool_name}" >&2 + exit 1 + fi +} + +printf 'Deploying release %s\n' "${RELEASE_TIMESTAMP}" + +require_tool dotnet +require_tool rsync +require_tool ssh + +mkdir -p "${LOCAL_PUBLISH_DIR}" + +printf '1) Publishing app locally...\n' +dotnet publish "${PROJECT_PATH}" -c Release -o "${LOCAL_PUBLISH_DIR}" + +cat > "${LOCAL_STAGE_DIR}/Dockerfile" <<'EOF' +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +ENV ASPNETCORE_URLS=http://+:8080 +ENV DOTNET_EnableDiagnostics=0 +EXPOSE 8080 +COPY publish/ ./ +RUN mkdir -p /app/data +ENTRYPOINT ["dotnet", "RpgRoller.dll"] +EOF + +printf '2) Preparing remote release directory...\n' +ssh "${REMOTE_HOST}" "mkdir -p '${REMOTE_RELEASES_DIR}' '${REMOTE_DATA_DIR}' && test ! -e '${REMOTE_RELEASE_DIR}'" + +printf '3) Uploading release payload...\n' +rsync -az --delete "${LOCAL_STAGE_DIR}/" "${REMOTE_HOST}:${REMOTE_RELEASE_DIR}/" + +printf '4) Building image and restarting container on remote host...\n' +ssh "${REMOTE_HOST}" "bash -se" </dev/null +fi + +if ! docker run -d \ + --name "\${container_name}" \ + --restart unless-stopped \ + -p "127.0.0.1:\${host_port}:\${container_port}" \ + -e ASPNETCORE_ENVIRONMENT=Production \ + -e ASPNETCORE_URLS="http://+:\${container_port}" \ + -e ConnectionStrings__RpgRoller="Data Source=/app/data/rpgroller.db" \ + -v "\${remote_data_dir}:/app/data" \ + "\${image_name}:\${release_timestamp}" >/dev/null; then + if [ -n "\${previous_current_target}" ]; then + ln -sfn "\${previous_current_target}" "\${remote_current_link}" + fi + exit 1 +fi +EOF + +printf '5) Deployment complete.\n'