Compare commits
7 Commits
4c3ed8a76c
...
b5d3a060f7
| Author | SHA1 | Date | |
|---|---|---|---|
| b5d3a060f7 | |||
| 995316c8ca | |||
| 0608cd3d10 | |||
| 40925868bc | |||
| 7a3169d77e | |||
| 61f11eed38 | |||
| 26960a8a15 |
@@ -15,7 +15,7 @@ These tools are installed and available: Python3, MiKTeX, Tesseract, Playwright
|
||||
- When an task is finished, perform a code review to evaluate if the change is clean and maintainable with high software engineering standards. Iterate on the code and repeat the review process until satisfied.
|
||||
- If there's documnentation present, always keep it updated.
|
||||
- After every iteration, evaluate if the test coverage would fall below 100%, and write tests if necessary.
|
||||
- After every iteration, run `dotnet jb cleanupcode --build=False '$file1' '$file2' ...` for every file you touched.
|
||||
- After every iteration, run `dotnet jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
|
||||
- After every frontend change, verify the results using an ephemeral Playwright run.
|
||||
- For ad hoc verification in this repo, do not default to `npx playwright test` with a temp spec outside the repo.
|
||||
- When browser verification needs the app running, launch the app against a temporary copy of `src\RolemasterDb.App\rolemaster.db` so verification does not mutate the canonical DB.
|
||||
|
||||
@@ -21,7 +21,7 @@ These tool paths should be used instead of any entry in the PATH environment var
|
||||
- After the implementation is finished, verify all changed files, and run `python D:\Code\crlf.py $file1 $file2 ...` only for files you recognize, in order to normalize all line endings of all touched files to CRLF.
|
||||
- If there's documnentation present, always keep it updated.
|
||||
- After every iteration, evaluate if the test coverage would fall below 100%, and write tests if necessary.
|
||||
- After every iteration, run `jb cleanupcode --build=False '$file1' '$file2' ...` for every file you touched.
|
||||
- After every iteration, run `jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
|
||||
- After every frontend change, verify the results using an ephemeral Playwright run.
|
||||
- For ad hoc verification in this repo, do not default to `npx playwright test` with a temp spec outside the repo.
|
||||
- Prefer a repo-local ephemeral Node script under `artifacts_verify/` that imports `playwright` with `require('playwright')` and drives the browser directly.
|
||||
|
||||
14
deploy/vserver.Dockerfile
Normal file
14
deploy/vserver.Dockerfile
Normal 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"]
|
||||
28
deploy/vserver.env
Normal file
28
deploy/vserver.env
Normal file
@@ -0,0 +1,28 @@
|
||||
# SSH host or alias from ~/.ssh/config.
|
||||
# This can also be a literal user@host value, for example root@myvserver.
|
||||
REMOTE_HOST=root@myvserver
|
||||
|
||||
# Remote filesystem layout.
|
||||
REMOTE_APP_DIR=/root/docker/rolemasterdb
|
||||
REMOTE_DATA_DIR=/root/docker/rolemasterdb/data
|
||||
|
||||
# Docker names on the remote host.
|
||||
IMAGE_NAME=rolemasterdb
|
||||
CONTAINER_NAME=rolemasterdb
|
||||
|
||||
# Bind the container only on loopback so nginx is the public entrypoint.
|
||||
HOST_BIND_ADDRESS=127.0.0.1
|
||||
|
||||
# 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
|
||||
28
deploy/vserver.env.example
Normal file
28
deploy/vserver.env.example
Normal file
@@ -0,0 +1,28 @@
|
||||
# SSH host or alias from ~/.ssh/config.
|
||||
# This can also be a literal user@host value, for example root@myvserver.
|
||||
REMOTE_HOST=root@myvserver
|
||||
|
||||
# Remote filesystem layout.
|
||||
REMOTE_APP_DIR=/root/docker/rolemasterdb
|
||||
REMOTE_DATA_DIR=/root/docker/rolemasterdb/data
|
||||
|
||||
# Docker names on the remote host.
|
||||
IMAGE_NAME=rolemasterdb
|
||||
CONTAINER_NAME=rolemasterdb
|
||||
|
||||
# Bind the container only on loopback so nginx is the public entrypoint.
|
||||
HOST_BIND_ADDRESS=127.0.0.1
|
||||
|
||||
# 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
|
||||
182
docs/vserver_docker_deploy.md
Normal file
182
docs/vserver_docker_deploy.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# 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 as `root`, for example with:
|
||||
|
||||
```bash
|
||||
ssh myvserver
|
||||
```
|
||||
|
||||
The intended public URL is:
|
||||
|
||||
```text
|
||||
https://rolemasterdb.franktovar.de
|
||||
```
|
||||
|
||||
The script publishes the app locally, uploads a release bundle over `scp`, builds the Docker image on the server, and replaces the running container. Nginx remains a one-time manual setup on the host and proxies to the container over localhost.
|
||||
|
||||
## Files
|
||||
|
||||
- `scripts/deploy-vserver.sh`
|
||||
- `deploy/vserver.Dockerfile`
|
||||
- `deploy/vserver.env.example`
|
||||
|
||||
## Remote Prerequisites
|
||||
|
||||
On the vserver:
|
||||
|
||||
1. Install Docker.
|
||||
2. Install nginx.
|
||||
3. Install a certificate tool such as `certbot` plus the nginx plugin if you want automated certificate provisioning.
|
||||
4. Make sure `rolemasterdb.franktovar.de` resolves to this server. Your wildcard `A` and `AAAA` records already satisfy that.
|
||||
|
||||
The deploy script stores releases under `REMOTE_APP_DIR/releases/<timestamp>` and keeps the SQLite database in `REMOTE_DATA_DIR/rolemaster.db`.
|
||||
The container should stay behind nginx, so the default deploy config binds it to `127.0.0.1:8080`.
|
||||
|
||||
## 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 `root@myvserver`
|
||||
- `REMOTE_APP_DIR`: base directory on the server, default `/root/docker/rolemasterdb`
|
||||
- `REMOTE_DATA_DIR`: persistent directory for the SQLite database
|
||||
- `HOST_BIND_ADDRESS`: default `127.0.0.1` so only nginx can reach the container
|
||||
- `HOST_PORT`: nginx upstream port on the host, default `8080`
|
||||
- `REMOTE_USE_SUDO`: keep `0` when you deploy as `root`
|
||||
- `REMOTE_ENV_FILE`: optional existing env file on the server to pass through to `docker run`
|
||||
|
||||
## One-Time Server Setup
|
||||
|
||||
Create the base directories on the server:
|
||||
|
||||
```bash
|
||||
ssh myvserver
|
||||
mkdir -p /root/docker/rolemasterdb/data
|
||||
```
|
||||
|
||||
Deploy once so the container exists:
|
||||
|
||||
```bash
|
||||
./scripts/deploy-vserver.sh
|
||||
```
|
||||
|
||||
Create an nginx site config at `/etc/nginx/sites-available/rolemasterdb.franktovar.de`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name rolemasterdb.franktovar.de;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
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;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 300;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Enable it and reload nginx:
|
||||
|
||||
```bash
|
||||
ln -s /etc/nginx/sites-available/rolemasterdb.franktovar.de /etc/nginx/sites-enabled/rolemasterdb.franktovar.de
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
```
|
||||
|
||||
At this point `http://rolemasterdb.franktovar.de` should work.
|
||||
|
||||
## HTTPS Setup
|
||||
|
||||
If `certbot` and the nginx plugin are installed, request the certificate:
|
||||
|
||||
```bash
|
||||
certbot --nginx -d rolemasterdb.franktovar.de
|
||||
```
|
||||
|
||||
That should update the nginx config to serve HTTPS and normally also install an HTTP-to-HTTPS redirect. Verify the generated config and then reload nginx if needed:
|
||||
|
||||
```bash
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
```
|
||||
|
||||
After that, `https://rolemasterdb.franktovar.de` should terminate TLS in nginx and proxy traffic to `127.0.0.1:8080`.
|
||||
|
||||
## 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. Replaces `publish/import-artifacts` with a full copy of `src/RolemasterDb.App/import-artifacts`.
|
||||
This is required because the current publish output does not reliably include deep artifact folders such as `cells/` and `pages/`.
|
||||
4. Bundles the publish output, `src/RolemasterDb.App/rolemaster.db`, and the runtime Dockerfile into `artifacts/deploy/<release-id>/`.
|
||||
5. Uploads the bundle to the vserver.
|
||||
6. Extracts the bundle on the vserver.
|
||||
7. Seeds `REMOTE_DATA_DIR/rolemaster.db` on first deploy only.
|
||||
8. Builds the Docker image on the vserver.
|
||||
9. Recreates the container with:
|
||||
|
||||
```text
|
||||
--restart unless-stopped
|
||||
-p HOST_BIND_ADDRESS: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`
|
||||
- keep the complete imported artifact tree inside the image for that release
|
||||
- rebuild the image and restart the container
|
||||
- leave the nginx config and TLS setup untouched
|
||||
|
||||
## Useful Remote Commands
|
||||
|
||||
Check the running container:
|
||||
|
||||
```bash
|
||||
ssh myvserver docker ps
|
||||
```
|
||||
|
||||
Follow logs:
|
||||
|
||||
```bash
|
||||
ssh myvserver docker logs -f rolemasterdb
|
||||
```
|
||||
|
||||
Check nginx:
|
||||
|
||||
```bash
|
||||
ssh myvserver nginx -t
|
||||
ssh myvserver systemctl status nginx
|
||||
```
|
||||
212
scripts/deploy-vserver.sh
Executable file
212
scripts/deploy-vserver.sh
Executable file
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd "$script_dir/.." && pwd)"
|
||||
|
||||
ssh_control_path=""
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
|
||||
if [[ -n "$ssh_control_path" ]] && ssh -o ControlPath="$ssh_control_path" -O exit "$REMOTE_HOST" >/dev/null 2>&1; then
|
||||
:
|
||||
fi
|
||||
|
||||
exit "$exit_code"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
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_BIND_ADDRESS="${HOST_BIND_ADDRESS:-}"
|
||||
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"
|
||||
import_artifacts_source_dir="$repo_root/src/RolemasterDb.App/import-artifacts"
|
||||
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"
|
||||
ssh_control_path="/tmp/rolemasterdb-deploy-${USER:-$(id -un)}-$(date -u +%Y%m%d%H%M%S)-$$.sock"
|
||||
|
||||
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
|
||||
|
||||
if [[ ! -d "$import_artifacts_source_dir" ]]; then
|
||||
echo "Import artifacts directory not found: $import_artifacts_source_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$artifact_root"
|
||||
|
||||
echo "Opening shared SSH connection to $REMOTE_HOST..."
|
||||
ssh \
|
||||
-o ControlMaster=yes \
|
||||
-o ControlPersist=10m \
|
||||
-o ControlPath="$ssh_control_path" \
|
||||
-Nf \
|
||||
"$REMOTE_HOST"
|
||||
|
||||
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 "Copying full import-artifacts tree into publish output..."
|
||||
rm -rf "$publish_dir/import-artifacts"
|
||||
cp -a "$import_artifacts_source_dir" "$publish_dir/import-artifacts"
|
||||
|
||||
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 \
|
||||
-o ControlPath="$ssh_control_path" \
|
||||
"$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 \
|
||||
-o ControlPath="$ssh_control_path" \
|
||||
"$tarball_path" "$REMOTE_HOST:$remote_tarball_path"
|
||||
|
||||
echo "Building image and restarting container on $REMOTE_HOST..."
|
||||
remote_env_file_arg="${REMOTE_ENV_FILE:-__ROLEMASTERDB_EMPTY__}"
|
||||
ssh \
|
||||
-o ControlPath="$ssh_control_path" \
|
||||
"$REMOTE_HOST" bash -s -- \
|
||||
"$release_id" \
|
||||
"$REMOTE_APP_DIR" \
|
||||
"$REMOTE_DATA_DIR" \
|
||||
"$IMAGE_NAME" \
|
||||
"$CONTAINER_NAME" \
|
||||
"$HOST_BIND_ADDRESS" \
|
||||
"$HOST_PORT" \
|
||||
"$CONTAINER_PORT" \
|
||||
"$REMOTE_USE_SUDO" \
|
||||
"$remote_env_file_arg" <<'EOF'
|
||||
set -euo pipefail
|
||||
|
||||
release_id="$1"
|
||||
remote_app_dir="$2"
|
||||
remote_data_dir="$3"
|
||||
image_name="$4"
|
||||
container_name="$5"
|
||||
host_bind_address="$6"
|
||||
host_port="$7"
|
||||
container_port="$8"
|
||||
remote_use_sudo="$9"
|
||||
remote_env_file="${10}"
|
||||
|
||||
if [[ "$remote_env_file" == "__ROLEMASTERDB_EMPTY__" ]]; then
|
||||
remote_env_file=""
|
||||
fi
|
||||
|
||||
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
|
||||
-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 "$host_bind_address" ]]; then
|
||||
docker_run_args+=(-p "$host_bind_address:$host_port:$container_port")
|
||||
else
|
||||
docker_run_args+=(-p "$host_port:$container_port")
|
||||
fi
|
||||
|
||||
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"
|
||||
if [[ -n "$host_bind_address" ]]; then
|
||||
echo "Published at: $host_bind_address:$host_port -> $container_port"
|
||||
else
|
||||
echo "Published at: $host_port -> $container_port"
|
||||
fi
|
||||
EOF
|
||||
|
||||
echo "Local bundle ready at $tarball_path"
|
||||
Reference in New Issue
Block a user