Add vserver Docker deploy script

This commit is contained in:
2026-04-26 19:30:24 +02:00
parent 4c3ed8a76c
commit 26960a8a15
4 changed files with 294 additions and 0 deletions

14
deploy/vserver.Dockerfile Normal file
View File

@@ -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"]

View File

@@ -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

View File

@@ -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/<timestamp>` 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/<release-id>/`.
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
```

158
scripts/deploy-vserver.sh Executable file
View File

@@ -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"