Initial commit: SQL Server 2025 local dev stack

This commit is contained in:
2026-01-30 15:16:51 +01:00
commit 4a905eeaaa
18 changed files with 534 additions and 0 deletions

4
scripts/down.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
docker compose down

36
scripts/fix-perms.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ts() { date +"%Y-%m-%d %H:%M:%S"; }
log() { echo "[$(ts)] [INFO] $*"; }
warn(){ echo "[$(ts)] [WARN] $*" >&2; }
err() { echo "[$(ts)] [ERROR] $*" >&2; }
die() { err "$*"; exit 1; }
cd "$(dirname "$0")/.."
if ! command -v docker >/dev/null 2>&1; then
die "docker is not installed or not in PATH."
fi
if ! docker info >/dev/null 2>&1; then
die "docker daemon not reachable. Start docker service first."
fi
log "Stopping/removing any existing stack (safe to run repeatedly)..."
# Not fatal if nothing is running
docker compose down --remove-orphans >/dev/null 2>&1 || true
# Additionally remove a stray container with the same name (outside this compose project)
if docker ps -a --format '{{.Names}}' | grep -qx 'mssql2025'; then
warn "Found stray container named mssql2025; removing it..."
docker rm -f mssql2025 >/dev/null 2>&1 || true
fi
log "Ensuring ./data and ./backup exist..."
mkdir -p ./data ./backup
log "Fixing permissions for ./data and ./backup (UID 10001)..."
sudo chown -R 10001:0 ./data ./backup
sudo chmod -R ug+rwx ./data ./backup
log "Permissions fixed ✅"

93
scripts/migrate.sh Executable file
View File

@@ -0,0 +1,93 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ts() { date +"%Y-%m-%d %H:%M:%S"; }
log() { echo "[$(ts)] [INFO] $*"; }
warn(){ echo "[$(ts)] [WARN] $*" >&2; }
err() { echo "[$(ts)] [ERROR] $*" >&2; }
die() { err "$*"; exit 1; }
trap 'err "Migration failed. Showing last 200 log lines:"; docker logs --tail=200 mssql2025 2>/dev/null || true' ERR
cd "$(dirname "$0")/.."
[[ -f .env ]] || die "Missing .env. Create it first: cp .env.example .env"
set -a
# shellcheck disable=SC1091
source .env
set +a
[[ -n "${MSSQL_SA_PASSWORD:-}" ]] || die "MSSQL_SA_PASSWORD is empty in .env"
if ! docker ps --format '{{.Names}}' | grep -qx 'mssql2025'; then
die "Container mssql2025 is not running. Start it first: ./scripts/start.sh"
fi
health="$(docker inspect -f '{{.State.Health.Status}}' mssql2025 2>/dev/null || true)"
[[ "$health" == "healthy" ]] || die "Container is not healthy (health=$health). Run ./scripts/start.sh"
# Bootstrap: ensure DevDb + migration table exists
bootstrap_sql=$(
cat <<'SQL'
IF DB_ID('DevDb') IS NULL
BEGIN
CREATE DATABASE DevDb;
END
GO
USE DevDb;
GO
IF OBJECT_ID('dbo.__SchemaMigrations','U') IS NULL
BEGIN
CREATE TABLE dbo.__SchemaMigrations(
Id int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Filename nvarchar(260) NOT NULL UNIQUE,
AppliedAt datetime2 NOT NULL CONSTRAINT DF___SchemaMigrations_AppliedAt DEFAULT (sysutcdatetime())
);
END
GO
SQL
)
log "Ensuring DevDb + dbo.__SchemaMigrations exist..."
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
-b -Q "$bootstrap_sql" < /dev/null
shopt -s nullglob
files=(db/migrations/*.sql)
if (( ${#files[@]} == 0 )); then
warn "No db/migrations/*.sql files found. Nothing to do."
exit 0
fi
log "Applying migrations (forward-only, each file once)."
for f in "${files[@]}"; do
bn="$(basename "$f")"
log "-> migration candidate: $bn"
check_sql="SET NOCOUNT ON; USE DevDb; SELECT COUNT(1) FROM dbo.__SchemaMigrations WHERE Filename = N'$bn';"
applied="$(docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" -h -1 -W -Q "$check_sql" < /dev/null \
| tr -d '\r' | tail -n 1 | xargs || true)"
if [[ "${applied:-0}" != "0" ]]; then
log " (skip) already applied"
continue
fi
log " (apply) running /migrations/$bn"
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
-b -i "/migrations/$bn" < /dev/null
record_sql="USE DevDb; INSERT INTO dbo.__SchemaMigrations(Filename) VALUES (N'$bn');"
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
-b -Q "$record_sql" < /dev/null
log " applied ✅"
done
log "Migrations completed ✅"

14
scripts/reset.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
echo "Stopping & removing container..."
docker compose down || true
echo "Deleting ./data (THIS REMOVES ALL DB DATA)..."
rm -rf ./data
mkdir -p ./data
./scripts/start.sh
./scripts/seed.sh
./scripts/migrate.sh

23
scripts/restart.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ts() { date +"%Y-%m-%d %H:%M:%S"; }
log() { echo "[$(ts)] [INFO] $*"; }
warn(){ echo "[$(ts)] [WARN] $*" >&2; }
err() { echo "[$(ts)] [ERROR] $*" >&2; }
die() { err "$*"; exit 1; }
cd "$(dirname "$0")/.."
[[ -x ./scripts/start.sh ]] || die "Missing ./scripts/start.sh"
[[ -x ./scripts/fix-perms.sh ]] || die "Missing ./scripts/fix-perms.sh"
log "Restarting SQL Server stack (fresh recreate)..."
# Make sure perms are correct and old instance is cleaned
./scripts/fix-perms.sh
# Start fresh
./scripts/start.sh
log "Restart completed ✅"

47
scripts/seed.sh Executable file
View File

@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ts() { date +"%Y-%m-%d %H:%M:%S"; }
log() { echo "[$(ts)] [INFO] $*"; }
warn(){ echo "[$(ts)] [WARN] $*" >&2; }
err() { echo "[$(ts)] [ERROR] $*" >&2; }
die() { err "$*"; exit 1; }
trap 'err "Seed failed. Showing last 200 log lines:"; docker logs --tail=200 mssql2025 2>/dev/null || true' ERR
cd "$(dirname "$0")/.."
[[ -f .env ]] || die "Missing .env. Create it first: cp .env.example .env"
set -a
# shellcheck disable=SC1091
source .env
set +a
[[ -n "${MSSQL_SA_PASSWORD:-}" ]] || die "MSSQL_SA_PASSWORD is empty in .env"
if ! docker ps --format '{{.Names}}' | grep -qx 'mssql2025'; then
die "Container mssql2025 is not running. Start it first: ./scripts/start.sh"
fi
health="$(docker inspect -f '{{.State.Health.Status}}' mssql2025 2>/dev/null || true)"
[[ "$health" == "healthy" ]] || die "Container is not healthy (health=$health). Run ./scripts/start.sh"
shopt -s nullglob
files=(db/seed/*.sql)
if (( ${#files[@]} == 0 )); then
warn "No db/seed/*.sql files found. Nothing to do."
exit 0
fi
log "Executing ${#files[@]} seed file(s) (idempotent)."
for f in "${files[@]}"; do
bn="$(basename "$f")"
log "-> seed: $bn"
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
-b -i "/seed/$bn" < /dev/null
done
log "Seed completed ✅"

117
scripts/start.sh Executable file
View File

@@ -0,0 +1,117 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# ---------- logging helpers ----------
ts() { date +"%Y-%m-%d %H:%M:%S"; }
log() { echo "[$(ts)] [INFO] $*"; }
warn(){ echo "[$(ts)] [WARN] $*" >&2; }
err() { echo "[$(ts)] [ERROR] $*" >&2; }
die() { err "$*"; exit 1; }
dump_diag() {
warn "Diagnostics:"
docker compose ps || true
echo
warn "docker ps (name=mssql2025):"
docker ps --filter "name=mssql2025" || true
echo
warn "Last 250 lines of container logs (mssql2025):"
docker logs --tail=250 mssql2025 2>/dev/null || warn "No logs available."
}
on_error() {
local exit_code=$?
err "Start failed (exit code=$exit_code)."
dump_diag
exit "$exit_code"
}
trap on_error ERR
# ---------- go to repo root ----------
cd "$(dirname "$0")/.."
# ---------- preflight ----------
command -v docker >/dev/null 2>&1 || die "docker is not installed or not in PATH."
docker info >/dev/null 2>&1 || die "docker daemon not reachable. Start docker service (sudo systemctl start docker)."
[[ -f .env ]] || die "Missing .env. Create it first: cp .env.example .env (then edit MSSQL_SA_PASSWORD)."
# Load .env
set -a
# shellcheck disable=SC1091
source .env
set +a
[[ -n "${MSSQL_SA_PASSWORD:-}" ]] || die "MSSQL_SA_PASSWORD is empty in .env"
# Validate MSSQL_PID for SQL Server 2025 containers
case "${MSSQL_PID:-Developer}" in
"Enterprise Developer"|"EnterpriseDeveloper"|"Enterprise_Developer")
cat >&2 <<'EOF'
[ERROR] MSSQL_PID is set to "Enterprise Developer", which SQL Server 2025 containers reject.
Fix: edit .env and set:
MSSQL_PID=Developer
(or: DeveloperStandard)
EOF
exit 2
;;
Developer|DeveloperStandard|Express|Evaluation|Standard|Enterprise)
;;
*)
warn "MSSQL_PID='${MSSQL_PID}' is unusual. If SQL Server errors about PID format, set MSSQL_PID=Developer."
;;
esac
log "Hard-resetting previous instance (if any)..."
# Bring down compose stack (remove orphans), but do NOT delete volumes because we use bind mounts (./data)
docker compose down --remove-orphans >/dev/null 2>&1 || true
# If there is a stray container with same name, kill it too (outside compose)
if docker ps -a --format '{{.Names}}' | grep -qx 'mssql2025'; then
warn "Found stray container named mssql2025; removing it..."
docker rm -f mssql2025 >/dev/null 2>&1 || true
fi
log "Starting fresh container..."
log "Settings: port=${MSSQL_PORT:-1433}, pid=${MSSQL_PID:-Developer}, cpuset=0-7, cpus=8.0"
# Ensure image is present (optional but nice)
docker compose pull >/dev/null 2>&1 || true
# Force recreate every time to avoid “stuck” states
docker compose up -d --force-recreate --remove-orphans
# ---------- wait for healthy ----------
log "Waiting for container healthcheck to become healthy..."
max_seconds=180
sleep_step=2
elapsed=0
while (( elapsed < max_seconds )); do
state="$(docker inspect -f '{{.State.Status}}' mssql2025 2>/dev/null || true)"
health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' mssql2025 2>/dev/null || true)"
printf "\r[%s] state=%-10s health=%-12s elapsed=%3ss/%ss" "$(ts)" "${state:-unknown}" "${health:-unknown}" "$elapsed" "$max_seconds"
if [[ "$health" == "healthy" ]]; then
echo
log "SQL Server is healthy ✅"
log "Connect: localhost,${MSSQL_PORT:-1433} (user: sa)"
exit 0
fi
if [[ "$state" == "exited" ]]; then
echo
err "Container exited before becoming healthy."
dump_diag
exit 1
fi
sleep "$sleep_step"
elapsed=$((elapsed + sleep_step))
done
echo
err "Timed out waiting for SQL Server to become healthy (${max_seconds}s)."
dump_diag
exit 1

4
scripts/stop.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
docker compose stop