commit 4a905eeaaa2ec5327bcc7ad6f32fcaa94d2b989b Author: Johannes Rest Date: Fri Jan 30 15:16:51 2026 +0100 Initial commit: SQL Server 2025 local dev stack diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..05d8cbe --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Copy to .env and adjust values. +# IMPORTANT: Use a strong SA password (complexity rules apply). +MSSQL_SA_PASSWORD=ChangeMe_UseARealP@ssw0rd! + +# Optional: host port (default 1433) +MSSQL_PORT=1433 + +# IMPORTANT (SQL Server 2025): +# Valid examples: Developer, DeveloperStandard, Express, Evaluation, Standard, Enterprise +# For local dev: Developer (free, non-prod) +MSSQL_PID=Developer diff --git a/.gitea/ISSUE_TEMPLATE/bug.yml b/.gitea/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..350f62e --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,20 @@ +name: Bug report +description: Report a problem +title: "[bug] " +labels: ["bug"] +body: + - type: textarea + id: what + attributes: + label: What happened? + description: What did you expect to happen? + placeholder: Steps, logs, screenshots... + validations: + required: true + - type: textarea + id: repro + attributes: + label: Reproduction + description: Minimal steps to reproduce + validations: + required: true diff --git a/.gitea/ISSUE_TEMPLATE/feature.yml b/.gitea/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..01b0143 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,12 @@ +name: Feature request +description: Suggest an improvement +title: "[feat] " +labels: ["enhancement"] +body: + - type: textarea + id: idea + attributes: + label: Idea + description: What should we add/change? + validations: + required: true diff --git a/.gitea/PULL_REQUEST_TEMPLATE/default.md b/.gitea/PULL_REQUEST_TEMPLATE/default.md new file mode 100644 index 0000000..91429e5 --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE/default.md @@ -0,0 +1,11 @@ +## What +Describe what this PR changes. + +## Why +Why is this change needed? + +## How to test +- [ ] `./scripts/start.sh` +- [ ] `./scripts/seed.sh` +- [ ] `./scripts/migrate.sh` +- [ ] T-SQL smoke test in VS Code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2625d33 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# --- SQL Server container persistent data (local only) --- +data/ +backup/ + +# --- secrets / local overrides --- +.env + +# --- editor / OS --- +.vscode/ +.idea/ +.DS_Store + +# --- logs --- +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..c54b201 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# SQL Server 2025 on Arch Linux (Docker) — localdev-ready + +This repo provides a reproducible local SQL Server 2025 setup using Docker Compose, +plus convenience scripts for start/stop and database provisioning ("betanken"). + +## Layout / Mini Convention + +- `compose.yml` + Docker Compose definition (SQL Server 2025 container). +- `.env.example` → copy to `.env` (not committed) + Holds your `MSSQL_SA_PASSWORD` and optional settings (port, edition). +- `scripts/` + Convenience scripts: + - `start.sh` — start container and wait until healthy + - `stop.sh` — stop container + - `down.sh` — remove container (keeps data on disk) + - `seed.sh` — run **all** `db/seed/*.sql` in order + - `migrate.sh` — run **all** `db/migrations/*.sql` in order + - `reset.sh` — delete `./data` (DESTROYS DB files), then start + seed + migrate +- `db/seed/` (idempotent) + Scripts that can be re-run safely. Typical content: + - create database(s) if missing + - create logins/users if missing + - minimal reference data + Naming: `001_*.sql`, `010_*.sql`, ... +- `db/migrations/` (forward-only) + Schema/data changes over time. Each file should be **run exactly once** per environment. + Naming: `2026-01-30_001_create_tables.sql`, `2026-02-05_002_add_index.sql`, ... +- `data/` (gitignored) + SQL Server data files mounted from the host. +- `backup/` (gitignored) + Place `.bak` files here if you want to restore from backups. + +### Recommended workflow + +1. Copy env: + ```bash + cp .env.example .env + # edit .env (set strong password!) + ``` + +2. Start server: + ```bash + ./scripts/start.sh + ``` + +3. Provision baseline: + ```bash + ./scripts/seed.sh + ./scripts/migrate.sh + ``` + +4. Work with T-SQL (VS Code): + - Install extension: **SQL Server (mssql)** (Microsoft) + - Connect: + - Server: `localhost` + - Port: value from `MSSQL_PORT` (default 1433) + - Auth: SQL Login + - User: `sa` + - Password: `MSSQL_SA_PASSWORD` + - Open any `.sql` file and run **Execute Query**. + +## Gitea-ready notes + +- `.env` is ignored (secrets). +- `data/` and `backup/` are ignored (local artifacts). +- Use the issue/PR templates under `.gitea/` for consistent collaboration. diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..070a8ec --- /dev/null +++ b/compose.yml @@ -0,0 +1,35 @@ +services: + mssql: + image: mcr.microsoft.com/mssql/server:2025-RTM-ubuntu-22.04 + container_name: mssql2025 + + env_file: + - .env + + environment: + ACCEPT_EULA: "Y" + # IMPORTANT (SQL Server 2025): + # Use "Developer" or "DeveloperStandard" (NOT "Enterprise Developer") + MSSQL_PID: "${MSSQL_PID:-Developer}" + + # Workaround for CPU topology/NUMA assert (sosnumap.cpp) + cpuset: "0-15" + cpus: "16.0" + + ports: + - "${MSSQL_PORT:-1433}:1433" + + volumes: + - ./data:/var/opt/mssql + - ./backup:/var/opt/mssql/backup + - ./db/seed:/seed:ro + - ./db/migrations:/migrations:ro + + healthcheck: + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \"$$MSSQL_SA_PASSWORD\" -Q \"SELECT 1\" || exit 1"] + interval: 10s + timeout: 5s + retries: 30 + start_period: 25s + + restart: unless-stopped diff --git a/db/migrations/2026-01-30_001_create_customer.sql b/db/migrations/2026-01-30_001_create_customer.sql new file mode 100644 index 0000000..006d0d1 --- /dev/null +++ b/db/migrations/2026-01-30_001_create_customer.sql @@ -0,0 +1,12 @@ +USE DevDb; +GO + +IF OBJECT_ID('dbo.Customer', 'U') IS NULL +BEGIN + CREATE TABLE dbo.Customer( + CustomerId int IDENTITY(1,1) NOT NULL PRIMARY KEY, + Name nvarchar(200) NOT NULL, + CreatedAt datetime2 NOT NULL CONSTRAINT DF_Customer_CreatedAt DEFAULT (sysutcdatetime()) + ); +END +GO diff --git a/db/seed/001_create_db.sql b/db/seed/001_create_db.sql new file mode 100644 index 0000000..d90bd18 --- /dev/null +++ b/db/seed/001_create_db.sql @@ -0,0 +1,5 @@ +IF DB_ID('DevDb') IS NULL +BEGIN + CREATE DATABASE DevDb; +END +GO diff --git a/db/seed/020_seed_customers.sql b/db/seed/020_seed_customers.sql new file mode 100644 index 0000000..4e2491e --- /dev/null +++ b/db/seed/020_seed_customers.sql @@ -0,0 +1,9 @@ +USE DevDb; +GO + +IF NOT EXISTS (SELECT 1 FROM dbo.Customer WHERE Name = N'Alice') + INSERT INTO dbo.Customer(Name) VALUES (N'Alice'); + +IF NOT EXISTS (SELECT 1 FROM dbo.Customer WHERE Name = N'Bob') + INSERT INTO dbo.Customer(Name) VALUES (N'Bob'); +GO diff --git a/scripts/down.sh b/scripts/down.sh new file mode 100755 index 0000000..8732cb3 --- /dev/null +++ b/scripts/down.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +docker compose down diff --git a/scripts/fix-perms.sh b/scripts/fix-perms.sh new file mode 100755 index 0000000..5ed3b12 --- /dev/null +++ b/scripts/fix-perms.sh @@ -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 ✅" diff --git a/scripts/migrate.sh b/scripts/migrate.sh new file mode 100755 index 0000000..9b4abc9 --- /dev/null +++ b/scripts/migrate.sh @@ -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 ✅" diff --git a/scripts/reset.sh b/scripts/reset.sh new file mode 100755 index 0000000..bdf19eb --- /dev/null +++ b/scripts/reset.sh @@ -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 diff --git a/scripts/restart.sh b/scripts/restart.sh new file mode 100755 index 0000000..0de8a6d --- /dev/null +++ b/scripts/restart.sh @@ -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 ✅" diff --git a/scripts/seed.sh b/scripts/seed.sh new file mode 100755 index 0000000..1c0c318 --- /dev/null +++ b/scripts/seed.sh @@ -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 ✅" diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100755 index 0000000..b59feb1 --- /dev/null +++ b/scripts/start.sh @@ -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 diff --git a/scripts/stop.sh b/scripts/stop.sh new file mode 100755 index 0000000..3b6cfa2 --- /dev/null +++ b/scripts/stop.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +docker compose stop