From 26960a8a157b8bf1edab59cbf69f838f26a29b04 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 26 Apr 2026 19:30:24 +0200 Subject: [PATCH] Add vserver Docker deploy script --- deploy/vserver.Dockerfile | 14 +++ deploy/vserver.env.example | 24 ++++++ docs/vserver_docker_deploy.md | 98 +++++++++++++++++++++ scripts/deploy-vserver.sh | 158 ++++++++++++++++++++++++++++++++++ 4 files changed, 294 insertions(+) create mode 100644 deploy/vserver.Dockerfile create mode 100644 deploy/vserver.env.example create mode 100644 docs/vserver_docker_deploy.md create mode 100755 scripts/deploy-vserver.sh diff --git a/deploy/vserver.Dockerfile b/deploy/vserver.Dockerfile new file mode 100644 index 0000000..d0580b0 --- /dev/null +++ b/deploy/vserver.Dockerfile @@ -0,0 +1,14 @@ +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", "RolemasterDb.App.dll"] diff --git a/deploy/vserver.env.example b/deploy/vserver.env.example new file mode 100644 index 0000000..157916d --- /dev/null +++ b/deploy/vserver.env.example @@ -0,0 +1,24 @@ +# SSH host or alias from ~/.ssh/config. +REMOTE_HOST=myvserver + +# Remote filesystem layout. +REMOTE_APP_DIR=/opt/rolemasterdb +REMOTE_DATA_DIR=/opt/rolemasterdb/data + +# Docker names on the remote host. +IMAGE_NAME=rolemasterdb +CONTAINER_NAME=rolemasterdb + +# Port mapping: host:container. +HOST_PORT=8080 +CONTAINER_PORT=8080 + +# Set to 1 if docker on the server must be run through sudo. +REMOTE_USE_SUDO=0 + +# Optional env file that already exists on the server and should be passed +# through to docker run with --env-file. +REMOTE_ENV_FILE= + +# Prefix used for the generated local tarball name. +RELEASE_PREFIX=rolemasterdb diff --git a/docs/vserver_docker_deploy.md b/docs/vserver_docker_deploy.md new file mode 100644 index 0000000..6a9551b --- /dev/null +++ b/docs/vserver_docker_deploy.md @@ -0,0 +1,98 @@ +# Docker Deployment To A Linux VServer + +This repo now includes a deployment script for shipping `RolemasterDb.App` to a remote Linux server that you reach with: + +```bash +ssh myvserver +``` + +The script publishes the app locally, uploads a release bundle over `scp`, builds the Docker image on the server, and replaces the running container. + +## Files + +- `scripts/deploy-vserver.sh` +- `deploy/vserver.Dockerfile` +- `deploy/vserver.env.example` + +## Remote Prerequisites + +On the vserver: + +1. Install Docker. +2. Make sure the SSH user can run `docker`, or set `REMOTE_USE_SUDO=1` in the deploy config. +3. Open the host port you want to expose, for example `8080`. + +The deploy script stores releases under `REMOTE_APP_DIR/releases/` and keeps the SQLite database in `REMOTE_DATA_DIR/rolemaster.db`. + +## Local Setup + +Create a local deploy config from the example: + +```bash +cp deploy/vserver.env.example deploy/vserver.env +``` + +Adjust the values in `deploy/vserver.env` as needed: + +- `REMOTE_HOST`: your SSH host or alias, for example `myvserver` +- `REMOTE_APP_DIR`: base directory on the server, for example `/opt/rolemasterdb` +- `REMOTE_DATA_DIR`: persistent directory for the SQLite database +- `HOST_PORT`: public port on the vserver +- `REMOTE_USE_SUDO`: set to `1` if remote Docker commands need `sudo` +- `REMOTE_ENV_FILE`: optional existing env file on the server to pass through to `docker run` + +## Deploy + +Run: + +```bash +./scripts/deploy-vserver.sh +``` + +Or point at a different config file: + +```bash +./scripts/deploy-vserver.sh /path/to/custom.env +``` + +## What The Script Does + +1. Publishes `src/RolemasterDb.App/RolemasterDb.App.csproj` in `Release`. +2. Precreates `publish/wwwroot/components/layout` and `publish/wwwroot/components/shared` before publish. + This is required because the current project otherwise fails to publish two JavaScript static assets. +3. Bundles the publish output, `src/RolemasterDb.App/rolemaster.db`, and the runtime Dockerfile into `artifacts/deploy//`. +4. Uploads the bundle to the vserver. +5. Extracts the bundle on the vserver. +6. Seeds `REMOTE_DATA_DIR/rolemaster.db` on first deploy only. +7. Builds the Docker image on the vserver. +8. Recreates the container with: + +```text +--restart unless-stopped +-p HOST_PORT:CONTAINER_PORT +-v REMOTE_DATA_DIR:/app/data +ConnectionStrings__RolemasterDb=Data Source=/app/data/rolemaster.db +``` + +## Updating The App Later + +Running the same deploy command again will: + +- publish a fresh release bundle +- upload a new timestamped release +- keep the existing database in `REMOTE_DATA_DIR` +- rebuild the image and restart the container + +## Useful Remote Commands + +Check the running container: + +```bash +ssh myvserver docker ps +``` + +Follow logs: + +```bash +ssh myvserver docker logs -f rolemasterdb +``` diff --git a/scripts/deploy-vserver.sh b/scripts/deploy-vserver.sh new file mode 100755 index 0000000..9215cf8 --- /dev/null +++ b/scripts/deploy-vserver.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/.." && pwd)" + +if [[ $# -gt 1 ]]; then + echo "Usage: $0 [deploy-config-file]" >&2 + exit 1 +fi + +config_path="${1:-$repo_root/deploy/vserver.env}" +if [[ ! -f "$config_path" ]]; then + echo "Missing deploy config: $config_path" >&2 + echo "Copy deploy/vserver.env.example to deploy/vserver.env and adjust it first." >&2 + exit 1 +fi + +set -a +source "$config_path" +set +a + +REMOTE_HOST="${REMOTE_HOST:-myvserver}" +REMOTE_APP_DIR="${REMOTE_APP_DIR:-/opt/rolemasterdb}" +REMOTE_DATA_DIR="${REMOTE_DATA_DIR:-$REMOTE_APP_DIR/data}" +IMAGE_NAME="${IMAGE_NAME:-rolemasterdb}" +CONTAINER_NAME="${CONTAINER_NAME:-rolemasterdb}" +HOST_PORT="${HOST_PORT:-8080}" +CONTAINER_PORT="${CONTAINER_PORT:-8080}" +REMOTE_USE_SUDO="${REMOTE_USE_SUDO:-0}" +REMOTE_ENV_FILE="${REMOTE_ENV_FILE:-}" +RELEASE_PREFIX="${RELEASE_PREFIX:-rolemasterdb}" + +release_id="$(date -u +%Y%m%d%H%M%S)" +artifact_root="$repo_root/artifacts/deploy/$release_id" +stage_dir="$artifact_root/stage" +tarball_path="$artifact_root/${RELEASE_PREFIX}-${release_id}.tar.gz" +publish_dir="$repo_root/src/RolemasterDb.App/bin/Release/net10.0/publish" +seed_db_path="$repo_root/src/RolemasterDb.App/rolemaster.db" +dockerfile_path="$repo_root/deploy/vserver.Dockerfile" +project_path="$repo_root/src/RolemasterDb.App/RolemasterDb.App.csproj" +remote_release_dir="$REMOTE_APP_DIR/releases/$release_id" +remote_tarball_path="$remote_release_dir/release.tar.gz" + +if [[ ! -f "$seed_db_path" ]]; then + echo "Seed database not found: $seed_db_path" >&2 + exit 1 +fi + +if [[ ! -f "$dockerfile_path" ]]; then + echo "Dockerfile not found: $dockerfile_path" >&2 + exit 1 +fi + +mkdir -p "$artifact_root" + +echo "Publishing RolemasterDb.App..." +rm -rf "$publish_dir" +mkdir -p \ + "$publish_dir/wwwroot/components/layout" \ + "$publish_dir/wwwroot/components/shared" +env DOTNET_CLI_HOME="${DOTNET_CLI_HOME:-/tmp}" \ + dotnet publish "$project_path" -c Release + +echo "Preparing deploy bundle..." +rm -rf "$stage_dir" +mkdir -p "$stage_dir/publish" "$stage_dir/seed" +cp -a "$publish_dir/." "$stage_dir/publish/" +cp "$seed_db_path" "$stage_dir/seed/rolemaster.db" +cp "$dockerfile_path" "$stage_dir/Dockerfile" +tar -C "$stage_dir" -czf "$tarball_path" . + +echo "Creating remote release directory..." +ssh "$REMOTE_HOST" bash -s -- "$remote_release_dir" "$REMOTE_DATA_DIR" <<'EOF' +set -euo pipefail +release_dir="$1" +data_dir="$2" +mkdir -p "$release_dir" "$data_dir" +EOF + +echo "Uploading bundle to $REMOTE_HOST..." +scp "$tarball_path" "$REMOTE_HOST:$remote_tarball_path" + +echo "Building image and restarting container on $REMOTE_HOST..." +ssh "$REMOTE_HOST" bash -s -- \ + "$release_id" \ + "$REMOTE_APP_DIR" \ + "$REMOTE_DATA_DIR" \ + "$IMAGE_NAME" \ + "$CONTAINER_NAME" \ + "$HOST_PORT" \ + "$CONTAINER_PORT" \ + "$REMOTE_USE_SUDO" \ + "$REMOTE_ENV_FILE" <<'EOF' +set -euo pipefail + +release_id="$1" +remote_app_dir="$2" +remote_data_dir="$3" +image_name="$4" +container_name="$5" +host_port="$6" +container_port="$7" +remote_use_sudo="$8" +remote_env_file="$9" + +release_dir="$remote_app_dir/releases/$release_id" +tarball_path="$release_dir/release.tar.gz" + +docker_cmd=(docker) +if [[ "$remote_use_sudo" == "1" ]]; then + docker_cmd=(sudo docker) +fi + +tar -xzf "$tarball_path" -C "$release_dir" +rm -f "$tarball_path" + +if [[ ! -f "$remote_data_dir/rolemaster.db" ]]; then + cp "$release_dir/seed/rolemaster.db" "$remote_data_dir/rolemaster.db" +fi + +"${docker_cmd[@]}" build \ + -t "$image_name:$release_id" \ + -t "$image_name:latest" \ + "$release_dir" + +"${docker_cmd[@]}" rm -f "$container_name" >/dev/null 2>&1 || true + +docker_run_args=( + run + -d + --name "$container_name" + --restart unless-stopped + -p "$host_port:$container_port" + -e "ASPNETCORE_ENVIRONMENT=Production" + -e "ASPNETCORE_URLS=http://+:$container_port" + -e "ConnectionStrings__RolemasterDb=Data Source=/app/data/rolemaster.db" + -v "$remote_data_dir:/app/data" +) + +if [[ -n "$remote_env_file" ]]; then + docker_run_args+=(--env-file "$remote_env_file") +fi + +docker_run_args+=("$image_name:$release_id") + +"${docker_cmd[@]}" "${docker_run_args[@]}" + +ln -sfn "$release_dir" "$remote_app_dir/current" + +echo "Deployment complete." +echo "Release: $release_id" +echo "Container: $container_name" +echo "Host port: $host_port" +EOF + +echo "Local bundle ready at $tarball_path"