Add Docker and SQL Server container phases

This commit is contained in:
Nick Beckley 2026-07-10 12:00:58 +00:00
parent 4655260986
commit d07b5cc575
16 changed files with 707 additions and 16 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
.env.* .env.*
!.env.example !.env.example
!.env.*.example !.env.*.example
!*.env.example
*.secret *.secret
*.secrets *.secrets
secrets/ secrets/

View File

@ -87,8 +87,8 @@ Future component installation scripts will live under `install/phases/`. They wi
```text ```text
install/phases/10-dotnet.sh install/phases/10-dotnet.sh
install/phases/20-sql-server.sh install/phases/20-sql-server.sh
install/phases/30-nginx.sh install/phases/30-docker.sh
install/phases/40-docker.sh install/phases/40-nginx.sh
``` ```
These component scripts do not exist yet. Until they do, rebuild steps remain manual and are documented in [docs/REBUILD-GUIDE.md](docs/REBUILD-GUIDE.md). These component scripts do not exist yet. Until they do, rebuild steps remain manual and are documented in [docs/REBUILD-GUIDE.md](docs/REBUILD-GUIDE.md).

View File

@ -32,12 +32,15 @@ Current planned areas include:
```text ```text
config/nginx/ config/nginx/
config/sql-server/
config/sqlserver/ config/sqlserver/
config/systemd/ config/systemd/
``` ```
Additional service directories may be added when those services are implemented. Additional service directories may be added when those services are implemented.
`config/sql-server/` contains the version-controlled Docker Compose definition and safe example environment file for the SQL Server container. The live environment file is stored outside the repository at `/etc/atlas/sql-server.env`.
## Security Considerations ## Security Considerations
Configuration templates must not contain live secrets. Secret-bearing live files should be stored outside the repository and referenced by documented paths or environment variable names. Configuration templates must not contain live secrets. Secret-bearing live files should be stored outside the repository and referenced by documented paths or environment variable names.

View File

@ -0,0 +1,29 @@
name: atlas-sql
services:
sql-server:
image: mcr.microsoft.com/mssql/server:2025-CU1-ubuntu-24.04
container_name: atlas-sql-server
restart: unless-stopped
env_file:
- /etc/atlas/sql-server.env
ports:
- "1433:1433"
volumes:
- /sql/mssql:/var/opt/mssql
healthcheck:
test:
[
"CMD-SHELL",
"/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \"$${MSSQL_SA_PASSWORD}\" -C -Q \"SELECT 1\" -b -o /dev/null"
]
interval: 10s
timeout: 5s
retries: 18
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"

View File

@ -0,0 +1,11 @@
# Example only. Do not put live SQL credentials in this repository.
#
# The live file is created at:
# /etc/atlas/sql-server.env
#
# Required live variables:
ACCEPT_EULA=Y
MSSQL_PID=Developer
MSSQL_TCP_PORT=1433
MSSQL_SA_PASSWORD=CHANGE_ME_DO_NOT_COMMIT

View File

@ -106,15 +106,30 @@ nginx installation, site layout, TLS certificate handling, and exposure rules ar
### Role of SQL Server ### Role of SQL Server
SQL Server is planned as the relational database platform for applications that require it. `/sql` is the intended data volume. Native SQL Server packages are not currently used because Microsoft does not publish a supported native SQL Server repository for Ubuntu 26.04. Atlas runs SQL Server 2025 Developer through Microsoft's official Linux container image instead.
SQL Server version, data paths, backup paths, service account model, authentication policy, maintenance jobs, and restore process are `TODO: Confirm during the relevant implementation phase.` - Compose project: `atlas-sql`
- Container: `atlas-sql-server`
- Image: `mcr.microsoft.com/mssql/server:2025-CU1-ubuntu-24.04`
- Digest: `sha256:698682bab57c02c42bc0aa274b158aeb242d8e9104149a7489628d5535805816`
- Edition: SQL Server 2025 Developer
- Product version: `17.0.4006.2`
- Persistent storage: `/sql/mssql`
- Secret environment file: `/etc/atlas/sql-server.env`
The live SQL administrator password is not stored in Git.
### Role of Docker ### Role of Docker
Docker is planned as a possible application runtime and tooling boundary. It should be introduced only after the runtime policy is documented. Docker Engine is installed from Docker's official signed Ubuntu APT repository for `resolute`.
Docker installation method, user access policy, network policy, volume locations, image retention, and compose layout are `TODO: Confirm during the relevant implementation phase.` - Docker Engine: `29.6.1`
- Docker Compose plugin: `v5.3.1`
- Docker Buildx plugin: `v0.35.0`
- containerd.io: `2.2.5`
- User `nick` is a member of the `docker` group.
A new login may be required before all shells pick up Docker group membership.
## Separation of Concerns ## Separation of Concerns
@ -128,6 +143,7 @@ The intended storage and responsibility model is:
- Build artefacts: `TODO: Confirm during the relevant implementation phase.` - Build artefacts: `TODO: Confirm during the relevant implementation phase.`
- Deployed applications: `TODO: Confirm during the relevant implementation phase.` - Deployed applications: `TODO: Confirm during the relevant implementation phase.`
- SQL data: `/sql` - SQL data: `/sql`
- SQL Server container storage: `/sql/mssql`
- Backups: `TODO: Confirm during the relevant implementation phase.` - Backups: `TODO: Confirm during the relevant implementation phase.`
- Temporary files: system temporary locations or script-created `mktemp` paths - Temporary files: system temporary locations or script-created `mktemp` paths
- Live secrets: outside this repository - Live secrets: outside this repository

View File

@ -10,6 +10,61 @@ This file records chronological Atlas build and infrastructure milestones. Do no
### Phase ### Phase
Phase 2 Docker and SQL Server container platform.
### Objective
Install Docker Engine and run SQL Server 2025 Developer using Microsoft's official container image with persistent storage on `/sql`.
### Changes
- Added `install/phases/30-docker.sh`.
- Installed Docker Engine `29.6.1` from Docker's official Ubuntu `resolute` APT repository.
- Installed Docker Compose plugin `v5.3.1` and Docker Buildx plugin `v0.35.0`.
- Added user `nick` to the `docker` group.
- Added `install/phases/20-sql-server.sh`.
- Added SQL Server Compose configuration under `config/sql-server/`.
- Added `scripts/sql/sqlcmd.sh`.
- Deployed SQL Server 2025 Developer container `atlas-sql-server`.
- Stored SQL Server data under `/sql/mssql`.
- Stored live SQL Server environment secrets outside Git at `/etc/atlas/sql-server.env`.
### Validation
- Docker hello-world test passed.
- Docker non-root validation passed with `sg docker`.
- SQL Server container reached healthy state.
- SQL version and edition queries passed.
- Temporary database create, insert, read, and drop test passed.
- Container restart and Docker service restart tests passed.
- Container recreation with persistent storage passed.
- SQL installer idempotency passed after correcting protected env-file detection.
### Problems
- Native SQL Server packages are not supported for Ubuntu 26.04, so the official container route was used.
- Initial SQL container recreation exposed an env-file permissions issue. The installer now checks the protected env file through sudo.
### Decisions
- Use Docker's official APT repository rather than Ubuntu `docker.io`.
- Use SQL Server image `mcr.microsoft.com/mssql/server:2025-CU1-ubuntu-24.04`.
- Bind SQL Server port `1433` for LAN development access without changing UFW or router rules.
### Git Commit
Not yet committed.
### Operator
Codex.
### Date
2026-07-10
### Phase
Phase 2 .NET development platform. Phase 2 .NET development platform.
### Objective ### Objective

View File

@ -9,6 +9,8 @@ This guide describes the planned rebuild path for Atlas. At Phase 1, the reposit
- System discovery script: present. - System discovery script: present.
- Host validation script: present. - Host validation script: present.
- .NET installation script: present. - .NET installation script: present.
- Docker installation script: present.
- SQL Server container installation script: present.
- Other component installation scripts: not yet created. - Other component installation scripts: not yet created.
- Configuration templates: not yet created. - Configuration templates: not yet created.
- Operational scripts: not yet created. - Operational scripts: not yet created.
@ -114,8 +116,10 @@ Planned bootstrap flow:
5. Run `scripts/system/validate-host.sh`. 5. Run `scripts/system/validate-host.sh`.
6. Run `install/bootstrap.sh --dry-run`. 6. Run `install/bootstrap.sh --dry-run`.
7. Run `install/phases/10-dotnet.sh` when .NET SDKs are required. 7. Run `install/phases/10-dotnet.sh` when .NET SDKs are required.
8. Run additional ordered installation scripts from `install/phases/` when they exist. 8. Run `install/phases/30-docker.sh`.
9. Run smoke tests from `tests/` when they exist. 9. Run `install/phases/20-sql-server.sh` after Docker is available.
10. Run additional ordered installation scripts from `install/phases/` when they exist.
11. Run smoke tests from `tests/` when they exist.
Bootstrap command sequence is `TODO: Confirm during the relevant implementation phase.` Bootstrap command sequence is `TODO: Confirm during the relevant implementation phase.`
@ -126,10 +130,30 @@ Planned scripts may include:
```text ```text
install/phases/10-dotnet.sh install/phases/10-dotnet.sh
install/phases/20-sql-server.sh install/phases/20-sql-server.sh
install/phases/30-nginx.sh install/phases/30-docker.sh
install/phases/40-docker.sh install/phases/40-nginx.sh
``` ```
Current Docker and SQL Server commands:
```bash
install/phases/30-docker.sh
install/phases/20-sql-server.sh
```
SQL Server operations:
```bash
sudo docker compose -p atlas-sql -f config/sql-server/compose.yml up -d
sudo docker compose -p atlas-sql -f config/sql-server/compose.yml stop
sudo docker compose -p atlas-sql -f config/sql-server/compose.yml restart
sudo docker ps --filter name=atlas-sql-server
sudo docker logs atlas-sql-server
sudo docker inspect -f '{{.State.Health.Status}}' atlas-sql-server
```
SQL Server live secrets are stored at `/etc/atlas/sql-server.env` and must not be committed.
These scripts do not exist yet. When created, each script must: These scripts do not exist yet. When created, each script must:
- Be idempotent. - Be idempotent.

View File

@ -30,8 +30,8 @@ Future phase scripts should use a two-digit numeric prefix followed by a short l
```text ```text
phases/10-dotnet.sh phases/10-dotnet.sh
phases/20-sql-server.sh phases/20-sql-server.sh
phases/30-nginx.sh phases/30-docker.sh
phases/40-docker.sh phases/40-nginx.sh
phases/90-validate-platform.sh phases/90-validate-platform.sh
``` ```
@ -52,6 +52,8 @@ Current Phase 2 installer:
```bash ```bash
install/phases/10-dotnet.sh install/phases/10-dotnet.sh
install/phases/20-sql-server.sh
install/phases/30-docker.sh
``` ```
The .NET installer installs only the required .NET SDK packages and verifies them with `dotnet --list-sdks`. The .NET installer installs only the required .NET SDK packages and verifies them with `dotnet --list-sdks`.
@ -63,6 +65,8 @@ Current installed SDKs:
.NET 8 is installed from the official Ubuntu .NET backports PPA. .NET 10 is installed from the built-in Ubuntu package feeds. .NET 8 is installed from the official Ubuntu .NET backports PPA. .NET 10 is installed from the built-in Ubuntu package feeds.
Docker is installed from Docker's official signed Ubuntu APT repository. SQL Server uses Microsoft's official SQL Server 2025 container image because native SQL Server packages are not supported on Ubuntu 26.04.
Runtime output belongs outside the repository: Runtime output belongs outside the repository:
- `/var/log/atlas/install/` - `/var/log/atlas/install/`

239
install/phases/20-sql-server.sh Executable file
View File

@ -0,0 +1,239 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
REPO_ROOT="$(cd "${INSTALL_DIR}/.." && pwd)"
# shellcheck source=install/lib/common.sh
source "${INSTALL_DIR}/lib/common.sh"
# shellcheck source=install/lib/logging.sh
source "${INSTALL_DIR}/lib/logging.sh"
atlas_set_script_name "20-sql-server.sh"
atlas_install_error_trap
COMPOSE_FILE="${REPO_ROOT}/config/sql-server/compose.yml"
COMPOSE_PROJECT="atlas-sql"
CONTAINER_NAME="atlas-sql-server"
SQL_IMAGE="mcr.microsoft.com/mssql/server:2025-CU1-ubuntu-24.04"
SQL_IMAGE_DIGEST="sha256:698682bab57c02c42bc0aa274b158aeb242d8e9104149a7489628d5535805816"
SQL_ENV_DIR="/etc/atlas"
SQL_ENV_FILE="${SQL_ENV_DIR}/sql-server.env"
SQL_ROOT="/sql/mssql"
SQL_DIRS=(data log secrets backup)
usage() {
cat <<'USAGE'
Usage: install/phases/20-sql-server.sh [options]
Deploy SQL Server 2025 Developer using Microsoft's official container image.
Requires Docker Engine and Docker Compose.
Options:
--help Show this help text.
--version Show script/framework version.
--dry-run Show intended actions without making changes.
--verbose Print additional diagnostic output.
USAGE
}
while [[ "$#" -gt 0 ]]; do
case "$1" in
--help)
usage
exit "${ATLAS_EXIT_OK}"
;;
--version)
atlas_print_version
exit "${ATLAS_EXIT_OK}"
;;
--dry-run)
atlas_enable_dry_run
;;
--verbose)
atlas_enable_verbose
;;
*)
atlas_usage_error "Unknown option: $1"
exit "${ATLAS_EXIT_USAGE}"
;;
esac
shift
done
docker_cmd() {
if docker info >/dev/null 2>&1; then
docker "$@"
else
sudo -n docker "$@"
fi
}
compose_cmd() {
if docker info >/dev/null 2>&1; then
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" "$@"
else
sudo -n docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" "$@"
fi
}
require_prerequisites() {
atlas_require_sudo_noninteractive || {
atlas_log_error "Passwordless non-interactive sudo is required"
exit "${ATLAS_EXIT_PREREQ}"
}
atlas_require_command docker
docker compose version >/dev/null 2>&1 || {
atlas_log_error "Docker Compose plugin is required"
exit "${ATLAS_EXIT_PREREQ}"
}
systemctl is-active --quiet docker || {
atlas_log_error "Docker service is not active"
exit "${ATLAS_EXIT_PREREQ}"
}
findmnt -rn --target /sql >/dev/null 2>&1 || {
atlas_log_error "/sql must be mounted before SQL Server deployment"
exit "${ATLAS_EXIT_PREREQ}"
}
}
pull_image() {
if docker_cmd image inspect "$SQL_IMAGE" >/dev/null 2>&1; then
atlas_log_ok "SQL Server image already present: ${SQL_IMAGE}"
elif [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: would pull ${SQL_IMAGE}"
else
docker_cmd pull "$SQL_IMAGE"
fi
}
inspect_sql_uid_gid() {
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
printf '10001:10001\n'
return 0
fi
docker_cmd run --rm --entrypoint /bin/bash "$SQL_IMAGE" -lc 'printf "%s:%s\n" "$(id -u mssql)" "$(id -g mssql)"'
}
ensure_storage() {
local uid_gid="$1"
atlas_log_info "Ensuring SQL Server persistent storage under ${SQL_ROOT}"
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: would create ${SQL_ROOT}/{data,log,secrets,backup}"
atlas_log_info "Dry-run mode: would chown directories to ${uid_gid}"
return 0
fi
sudo -n install -d -m 0770 "$SQL_ROOT"
local dir
for dir in "${SQL_DIRS[@]}"; do
sudo -n install -d -m 0770 "${SQL_ROOT}/${dir}"
done
sudo -n chown "$uid_gid" "$SQL_ROOT" "${SQL_ROOT}/data" "${SQL_ROOT}/log" "${SQL_ROOT}/secrets" "${SQL_ROOT}/backup"
sudo -n chmod 0770 "$SQL_ROOT" "${SQL_ROOT}/data" "${SQL_ROOT}/log" "${SQL_ROOT}/secrets" "${SQL_ROOT}/backup"
}
generate_password() {
python3 - <<'PY'
import secrets
alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"
print("A1a_" + "".join(secrets.choice(alphabet) for _ in range(28)))
PY
}
ensure_env_file() {
if sudo -n test -f "$SQL_ENV_FILE"; then
atlas_log_ok "SQL Server environment file already exists"
return 0
fi
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: would create protected ${SQL_ENV_FILE}"
return 0
fi
local password tmp_file
password="$(generate_password)"
tmp_file="$(mktemp)"
cat >"$tmp_file" <<EOF
ACCEPT_EULA=Y
MSSQL_PID=Developer
MSSQL_TCP_PORT=1433
MSSQL_SA_PASSWORD=${password}
EOF
sudo -n install -d -m 0750 "$SQL_ENV_DIR"
sudo -n install -m 0640 -o root -g docker "$tmp_file" "$SQL_ENV_FILE"
rm -f "$tmp_file"
atlas_log_warn "Created ${SQL_ENV_FILE}; the generated SQL administrator password was not printed"
}
start_sql_server() {
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: would run docker compose up -d"
return 0
fi
if docker_cmd inspect "$CONTAINER_NAME" >/dev/null 2>&1; then
local state image
state="$(docker_cmd inspect -f '{{.State.Status}}' "$CONTAINER_NAME")"
image="$(docker_cmd inspect -f '{{.Config.Image}}' "$CONTAINER_NAME")"
if [[ "$image" == "$SQL_IMAGE" ]]; then
if [[ "$state" == "running" ]]; then
atlas_log_ok "SQL Server container is already running"
else
atlas_log_info "Starting existing SQL Server container"
docker_cmd start "$CONTAINER_NAME" >/dev/null
fi
return 0
fi
atlas_log_error "Existing ${CONTAINER_NAME} uses unexpected image: ${image}"
exit "${ATLAS_EXIT_UNSAFE}"
fi
compose_cmd up -d
}
wait_for_health() {
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: would wait for SQL Server health check"
return 0
fi
local status
for _ in {1..60}; do
status="$(docker_cmd inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$CONTAINER_NAME" 2>/dev/null || true)"
case "$status" in
healthy)
atlas_log_ok "SQL Server container is healthy"
return 0
;;
unhealthy)
atlas_log_error "SQL Server container health check is unhealthy"
docker_cmd logs --tail 80 "$CONTAINER_NAME" >&2 || true
exit "${ATLAS_EXIT_VALIDATION}"
;;
esac
sleep 5
done
atlas_log_error "Timed out waiting for SQL Server container health"
docker_cmd logs --tail 80 "$CONTAINER_NAME" >&2 || true
exit "${ATLAS_EXIT_VALIDATION}"
}
verify_sqlcmd() {
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: would verify sqlcmd connection"
return 0
fi
"${REPO_ROOT}/scripts/sql/sqlcmd.sh" -Q "SELECT @@VERSION;" >/dev/null
atlas_log_ok "SQL Server local sqlcmd verification passed"
}
main() {
require_prerequisites
pull_image
uid_gid="$(inspect_sql_uid_gid)"
ensure_storage "$uid_gid"
ensure_env_file
start_sql_server
wait_for_health
verify_sqlcmd
}
main

210
install/phases/30-docker.sh Executable file
View File

@ -0,0 +1,210 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=install/lib/common.sh
source "${INSTALL_DIR}/lib/common.sh"
# shellcheck source=install/lib/logging.sh
source "${INSTALL_DIR}/lib/logging.sh"
# shellcheck source=install/lib/packages.sh
source "${INSTALL_DIR}/lib/packages.sh"
atlas_set_script_name "30-docker.sh"
atlas_install_error_trap
DOCKER_KEYRING="/etc/apt/keyrings/docker.asc"
DOCKER_SOURCE="/etc/apt/sources.list.d/docker.list"
DOCKER_PACKAGES=(docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin)
DOCKER_CONFLICTS=(docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc)
DOCKER_REPO_BASE="https://download.docker.com/linux/ubuntu"
DOCKER_USER="nick"
usage() {
cat <<'USAGE'
Usage: install/phases/30-docker.sh [options]
Install Docker Engine from Docker's official signed Ubuntu APT repository.
Options:
--help Show this help text.
--version Show script/framework version.
--dry-run Show intended actions without making changes.
--verbose Print additional diagnostic output.
USAGE
}
while [[ "$#" -gt 0 ]]; do
case "$1" in
--help)
usage
exit "${ATLAS_EXIT_OK}"
;;
--version)
atlas_print_version
exit "${ATLAS_EXIT_OK}"
;;
--dry-run)
atlas_enable_dry_run
;;
--verbose)
atlas_enable_verbose
;;
*)
atlas_usage_error "Unknown option: $1"
exit "${ATLAS_EXIT_USAGE}"
;;
esac
shift
done
load_os_release() {
# shellcheck disable=SC1091
. /etc/os-release
if [[ "${ID:-}" != "ubuntu" || "${VERSION_CODENAME:-}" != "resolute" ]]; then
atlas_log_error "Unsupported OS for this Docker phase: ${PRETTY_NAME:-unknown}"
exit "${ATLAS_EXIT_PREREQ}"
fi
}
docker_repo_available() {
curl -fsSL "${DOCKER_REPO_BASE}/dists/${VERSION_CODENAME}/Release" >/dev/null
}
install_prerequisites() {
local packages=(ca-certificates curl)
local missing=()
local package_name
for package_name in "${packages[@]}"; do
atlas_package_installed "$package_name" || missing+=("$package_name")
done
if [[ "${#missing[@]}" -gt 0 ]]; then
atlas_install_packages "${missing[@]}"
else
atlas_log_ok "Docker repository prerequisites are already installed"
fi
}
remove_conflicts() {
local installed=()
local package_name
for package_name in "${DOCKER_CONFLICTS[@]}"; do
atlas_package_installed "$package_name" && installed+=("$package_name")
done
if [[ "${#installed[@]}" -eq 0 ]]; then
atlas_log_ok "No conflicting Docker packages are installed"
return 0
fi
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: would remove conflicting packages: ${installed[*]}"
return 0
fi
atlas_log_info "Removing conflicting Docker packages: ${installed[*]}"
atlas_sudo_run apt-get remove -y "${installed[@]}"
}
configure_repository() {
local arch repo_line
arch="$(dpkg --print-architecture)"
repo_line="deb [arch=${arch} signed-by=${DOCKER_KEYRING}] ${DOCKER_REPO_BASE} ${VERSION_CODENAME} stable"
if [[ -r "$DOCKER_SOURCE" ]] && grep -Fqx "$repo_line" "$DOCKER_SOURCE" && [[ -r "$DOCKER_KEYRING" ]]; then
atlas_log_ok "Docker APT repository is already configured"
return 0
fi
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: would install Docker GPG key to ${DOCKER_KEYRING}"
atlas_log_info "Dry-run mode: would write Docker APT source ${DOCKER_SOURCE}"
atlas_log_info "Dry-run mode: would run apt-get update"
return 0
fi
atlas_sudo_run install -m 0755 -d /etc/apt/keyrings
tmp_key="$(mktemp)"
curl -fsSL "${DOCKER_REPO_BASE}/gpg" -o "$tmp_key"
atlas_sudo_run install -m 0644 "$tmp_key" "$DOCKER_KEYRING"
rm -f "$tmp_key"
printf '%s\n' "$repo_line" | sudo -n tee "$DOCKER_SOURCE" >/dev/null
atlas_sudo_run apt-get update
}
install_docker_packages() {
local missing=()
local package_name
for package_name in "${DOCKER_PACKAGES[@]}"; do
atlas_package_installed "$package_name" || missing+=("$package_name")
done
if [[ "${#missing[@]}" -eq 0 ]]; then
atlas_log_ok "Docker packages are already installed"
else
atlas_install_packages "${missing[@]}"
fi
}
enable_docker_service() {
atlas_log_info "Ensuring Docker service is enabled and active"
atlas_sudo_run systemctl enable docker
atlas_sudo_run systemctl start docker
}
ensure_docker_group() {
if id -nG "$DOCKER_USER" | tr ' ' '\n' | grep -qx docker; then
atlas_log_ok "User ${DOCKER_USER} is already in the docker group"
return 0
fi
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: would add ${DOCKER_USER} to docker group"
return 0
fi
atlas_sudo_run usermod -aG docker "$DOCKER_USER"
atlas_log_warn "User ${DOCKER_USER} was added to docker group; a new login may be required"
}
verify_docker() {
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: skipping Docker runtime verification"
return 0
fi
docker --version
docker compose version
docker buildx version
systemctl is-enabled docker >/dev/null
systemctl is-active docker >/dev/null
if docker info >/dev/null 2>&1; then
atlas_log_ok "Docker is available to the current shell without sudo"
else
atlas_log_warn "Current shell cannot use Docker without sudo; a new login or sg docker may be required"
sudo -n docker info >/dev/null
fi
atlas_log_ok "Docker verification passed"
}
main() {
load_os_release
atlas_require_sudo_noninteractive || {
atlas_log_error "Passwordless non-interactive sudo is required"
exit "${ATLAS_EXIT_PREREQ}"
}
atlas_require_command curl
atlas_require_command dpkg
atlas_require_command apt-get
atlas_require_command systemctl
docker_repo_available || {
atlas_log_error "Docker official repository is not available for ${VERSION_CODENAME}"
exit "${ATLAS_EXIT_PREREQ}"
}
install_prerequisites
remove_conflicts
configure_repository
install_docker_packages
enable_docker_service
ensure_docker_group
verify_docker
}
main

View File

@ -2,7 +2,7 @@
This directory is reserved for future ordered Atlas installation phases. This directory is reserved for future ordered Atlas installation phases.
Phase 2A intentionally did not create speculative .NET, SQL Server, nginx, or Docker installer scripts. Phase 2 adds the .NET installer only. Phase 2A intentionally did not create speculative .NET, SQL Server, nginx, or Docker installer scripts. Phase 2 adds the .NET, Docker, and SQL Server container installers.
## Naming Convention ## Naming Convention
@ -11,8 +11,8 @@ Use a two-digit phase number and lower-case hyphenated component name:
```text ```text
10-dotnet.sh 10-dotnet.sh
20-sql-server.sh 20-sql-server.sh
30-nginx.sh 30-docker.sh
40-docker.sh 40-nginx.sh
90-validate-platform.sh 90-validate-platform.sh
``` ```
@ -21,8 +21,17 @@ Each phase must source the shared framework under `install/lib/`, support dry-ru
## Current Phases ## Current Phases
- `10-dotnet.sh`: installs the required .NET 8 and .NET 10 SDKs from official Ubuntu package sources. - `10-dotnet.sh`: installs the required .NET 8 and .NET 10 SDKs from official Ubuntu package sources.
- `20-sql-server.sh`: deploys SQL Server 2025 Developer using Microsoft's official container image.
- `30-docker.sh`: installs Docker Engine from Docker's official Ubuntu APT repository.
Installed SDKs after the current Phase 2 run: Installed SDKs after the current Phase 2 run:
- .NET 8 SDK `8.0.128` - .NET 8 SDK `8.0.128`
- .NET 10 SDK `10.0.109` - .NET 10 SDK `10.0.109`
Current SQL Server deployment:
- Image: `mcr.microsoft.com/mssql/server:2025-CU1-ubuntu-24.04`
- Digest: `sha256:698682bab57c02c42bc0aa274b158aeb242d8e9104149a7489628d5535805816`
- Container: `atlas-sql-server`
- Compose project: `atlas-sql`

View File

@ -39,6 +39,7 @@ scripts/deploy/
scripts/maintenance/ scripts/maintenance/
scripts/monitoring/ scripts/monitoring/
scripts/restore/ scripts/restore/
scripts/sql/
scripts/system/ scripts/system/
``` ```
@ -51,3 +52,4 @@ Additional areas may be added when a repeated operational need exists.
- Make destructive actions explicit and documented. - Make destructive actions explicit and documented.
- Include validation and rollback guidance for service-affecting scripts. - Include validation and rollback guidance for service-affecting scripts.
- Discovery and validation scripts must not dump environment variables, private keys, tokens, or complete sensitive configuration files. - Discovery and validation scripts must not dump environment variables, private keys, tokens, or complete sensitive configuration files.
- SQL helper scripts must not embed or print SQL administrator passwords.

34
scripts/sql/sqlcmd.sh Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONTAINER_NAME="atlas-sql-server"
SQLCMD_PATH="/opt/mssql-tools18/bin/sqlcmd"
usage() {
cat <<'USAGE'
Usage: scripts/sql/sqlcmd.sh [sqlcmd arguments]
Runs sqlcmd inside the Atlas SQL Server container without embedding the SQL
administrator password in the repository or command line.
Examples:
scripts/sql/sqlcmd.sh -Q "SELECT @@VERSION;"
scripts/sql/sqlcmd.sh -d master -Q "SELECT name FROM sys.databases;"
USAGE
}
if [[ "${1:-}" == "--help" ]]; then
usage
exit 0
fi
if ! command -v docker >/dev/null 2>&1; then
echo "ERROR: docker command not found" >&2
exit 3
fi
exec sudo -n docker exec -i "$CONTAINER_NAME" /bin/bash -lc \
'SQLCMDPASSWORD="$MSSQL_SA_PASSWORD" exec /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -C "$@"' \
sqlcmd "$@"

View File

@ -113,6 +113,24 @@ component_version() {
fi fi
} }
docker_status() {
if ! have docker; then
printf 'not installed\n'
elif sudo -n docker info >/dev/null 2>&1 || docker info >/dev/null 2>&1; then
printf 'active\n'
else
printf 'installed-not-active-or-not-accessible\n'
fi
}
sql_container_health() {
if ! have docker; then
printf 'docker unavailable\n'
else
sudo -n docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' atlas-sql-server 2>/dev/null || printf 'not present\n'
fi
}
mount_status() { mount_status() {
local path="$1" local path="$1"
if have findmnt && findmnt -rn --target "$path" >/dev/null 2>&1; then if have findmnt && findmnt -rn --target "$path" >/dev/null 2>&1; then
@ -223,6 +241,8 @@ human_report() {
printf ' nginx: %s\n' "$(component_version nginx -v)" printf ' nginx: %s\n' "$(component_version nginx -v)"
printf ' Docker: %s\n' "$(component_version docker --version)" printf ' Docker: %s\n' "$(component_version docker --version)"
printf ' Docker Compose: %s\n\n' "$(if have docker; then docker compose version 2>/dev/null | sed -n '1p'; else printf 'not installed'; fi)" printf ' Docker Compose: %s\n\n' "$(if have docker; then docker compose version 2>/dev/null | sed -n '1p'; else printf 'not installed'; fi)"
printf ' Docker status: %s\n' "$(docker_status)"
printf ' SQL Server container health: %s\n\n' "$(sql_container_health)"
printf 'Services\n' printf 'Services\n'
printf ' Failed systemd services:\n' printf ' Failed systemd services:\n'
@ -283,6 +303,8 @@ json_report() {
--arg sqlserver_version "$(component_version sqlservr --version)" \ --arg sqlserver_version "$(component_version sqlservr --version)" \
--arg nginx_version "$(component_version nginx -v)" \ --arg nginx_version "$(component_version nginx -v)" \
--arg docker_version "$(component_version docker --version)" \ --arg docker_version "$(component_version docker --version)" \
--arg docker_status "$(docker_status)" \
--arg sql_container_health "$(sql_container_health)" \
--arg hostname "$(hostname)" \ --arg hostname "$(hostname)" \
--arg host_overrides "$(local_host_overrides_present)" \ --arg host_overrides "$(local_host_overrides_present)" \
--arg firewall "$(firewall_summary)" \ --arg firewall "$(firewall_summary)" \
@ -309,7 +331,9 @@ json_report() {
dotnet: $dotnet_version, dotnet: $dotnet_version,
sql_server: $sqlserver_version, sql_server: $sqlserver_version,
nginx: $nginx_version, nginx: $nginx_version,
docker: $docker_version docker: $docker_version,
docker_status: $docker_status,
sql_server_container_health: $sql_container_health
}, },
services: {failed_systemd_units: ($failed_services | split("\n") | map(select(length > 0)))}, services: {failed_systemd_units: ($failed_services | split("\n") | map(select(length > 0)))},
network: { network: {

View File

@ -217,6 +217,35 @@ check_failed_services() {
fi fi
} }
check_docker_sql_state() {
if ! command -v docker >/dev/null 2>&1; then
atlas_validation_result SKIPPED "Docker is not installed"
return
fi
if systemctl is-active --quiet docker; then
atlas_validation_result PASS "Docker service is active"
else
atlas_validation_result FAIL "Docker service is not active"
fi
if docker compose version >/dev/null 2>&1; then
atlas_validation_result PASS "Docker Compose plugin is available"
else
atlas_validation_result FAIL "Docker Compose plugin is not available"
fi
local sql_health
sql_health="$(sudo -n docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' atlas-sql-server 2>/dev/null || true)"
if [[ -z "$sql_health" ]]; then
atlas_validation_result SKIPPED "SQL Server container is not present"
elif [[ "$sql_health" == "healthy" ]]; then
atlas_validation_result PASS "SQL Server container is healthy"
else
atlas_validation_result FAIL "SQL Server container health is ${sql_health}"
fi
}
atlas_validation_reset atlas_validation_reset
printf 'Atlas Host Validation\n' printf 'Atlas Host Validation\n'
@ -260,6 +289,7 @@ check_free_space /srv "$MIN_SRV_FREE_GIB"
check_free_space /sql "$MIN_SQL_FREE_GIB" check_free_space /sql "$MIN_SQL_FREE_GIB"
check_failed_services check_failed_services
check_docker_sql_state
if [[ -f /var/run/reboot-required ]]; then if [[ -f /var/run/reboot-required ]]; then
atlas_validation_result WARNING "Reboot is pending" atlas_validation_result WARNING "Reboot is pending"