Compare commits

...

2 Commits

Author SHA1 Message Date
ec8ce94082 Add PlotDirector development deployment 2026-07-10 18:43:13 +00:00
4dafddf8e5 Add nginx and firewall baseline 2026-07-10 18:42:43 +00:00
18 changed files with 773 additions and 29 deletions

View File

@ -41,6 +41,10 @@ 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`. `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`.
`config/nginx/` contains the temporary Atlas default HTTP site configuration and the PlotDirector development reverse-proxy configuration. Live nginx configuration is deployed under `/etc/nginx/`.
`config/systemd/` contains source-controlled systemd unit templates. `plotdirector-dev.service` is deployed to `/etc/systemd/system/plotdirector-dev.service`.
## 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.

21
config/nginx/README.md Normal file
View File

@ -0,0 +1,21 @@
# nginx Configuration
This directory contains version-controlled nginx configuration for Atlas.
`atlas-default.conf` is the temporary default HTTP site. It serves a small static status page from `/var/www/atlas` and exposes `/health` for local validation.
`plotdirector-dev.conf` proxies `dev.plotdirector.com` to PlotDirector on `http://127.0.0.1:5050` and provides a lightweight `/health` endpoint. It is intentionally HTTP-only until the TLS phase.
Deployment locations:
- Source configuration: `config/nginx/atlas-default.conf`
- Live configuration: `/etc/nginx/sites-available/atlas-default.conf`
- Enabled site: `/etc/nginx/sites-enabled/atlas-default.conf`
- Static content: `/var/www/atlas`
- Health check: `/health`
- PlotDirector source configuration: `config/nginx/plotdirector-dev.conf`
- PlotDirector live configuration: `/etc/nginx/sites-available/plotdirector-dev.conf`
- PlotDirector enabled site: `/etc/nginx/sites-enabled/plotdirector-dev.conf`
- PlotDirector hostname: `dev.plotdirector.com`
Do not store TLS private keys, live certificates, secrets, or PlotDirector-specific proxy credentials here.

View File

@ -0,0 +1,20 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /var/www/atlas;
index index.html;
access_log /var/log/nginx/atlas-access.log;
error_log /var/log/nginx/atlas-error.log;
location = /health {
add_header Content-Type text/plain;
return 200 "ok\n";
}
location / {
try_files $uri $uri/ =404;
}
}

View File

@ -0,0 +1,30 @@
server {
listen 80;
listen [::]:80;
server_name dev.plotdirector.com;
access_log /var/log/nginx/plotdirector-dev-access.log;
error_log /var/log/nginx/plotdirector-dev-error.log;
client_max_body_size 100m;
location = /health {
add_header Content-Type text/plain;
return 200 "ok\n";
}
location / {
proxy_pass http://127.0.0.1:5050;
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 120s;
proxy_send_timeout 120s;
proxy_connect_timeout 30s;
}
}

View File

@ -8,7 +8,7 @@ services:
env_file: env_file:
- /etc/atlas/sql-server.env - /etc/atlas/sql-server.env
ports: ports:
- "1433:1433" - "192.168.10.10:1433:1433"
volumes: volumes:
- /sql/mssql:/var/opt/mssql - /sql/mssql:/var/opt/mssql
healthcheck: healthcheck:
@ -26,4 +26,3 @@ services:
options: options:
max-size: "10m" max-size: "10m"
max-file: "5" max-file: "5"

View File

@ -8,4 +8,3 @@ ACCEPT_EULA=Y
MSSQL_PID=Developer MSSQL_PID=Developer
MSSQL_TCP_PORT=1433 MSSQL_TCP_PORT=1433
MSSQL_SA_PASSWORD=CHANGE_ME_DO_NOT_COMMIT MSSQL_SA_PASSWORD=CHANGE_ME_DO_NOT_COMMIT

View File

@ -0,0 +1,21 @@
[Unit]
Description=PlotDirector development web application
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
User=plotdirector
Group=plotdirector
WorkingDirectory=/srv/apps/plotdirector-dev/current
EnvironmentFile=/etc/plotdirector/plotdirector.env
ExecStart=/usr/bin/dotnet /srv/apps/plotdirector-dev/current/PlotLine.dll
Restart=on-failure
RestartSec=10
SyslogIdentifier=plotdirector-dev
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target

View File

@ -23,6 +23,8 @@ Atlas is a single physical development server.
- Development volume: SSD mounted at `/srv` - Development volume: SSD mounted at `/srv`
- Repository root: `/srv/repos` - Repository root: `/srv/repos`
- Infrastructure repository: `/srv/repos/AI-Development-Server` - Infrastructure repository: `/srv/repos/AI-Development-Server`
- PlotDirector source repository: `/srv/repos/PlotDirector`
- PlotDirector development deployment: `/srv/apps/plotdirector-dev/current`
The intended model separates operating system state, SQL data, and development data. This supports rebuild and recovery by reducing reliance on the OS volume for mutable project data. The intended model separates operating system state, SQL data, and development data. This supports rebuild and recovery by reducing reliance on the OS volume for mutable project data.
@ -34,7 +36,9 @@ The intended model separates operating system state, SQL data, and development d
- Primary network interface: `enp3s0` - Primary network interface: `enp3s0`
- Gitea SSH remote for this repository: `git@git.catherinelynwood.com:atlas-bot/AI-Development-Server.git` - Gitea SSH remote for this repository: `git@git.catherinelynwood.com:atlas-bot/AI-Development-Server.git`
DNS, firewall, public exposure, and TLS policy are `TODO: Confirm during the relevant implementation phase.` UFW is active with default deny incoming and default allow outgoing. Inbound SSH `22`, HTTP `80`, and HTTPS `443` are allowed. SQL Server `1433` is allowed only from `192.168.10.0/24` and is bound to `192.168.10.10`.
Public DNS, TLS, and router exposure policy are `TODO: Confirm during the relevant implementation phase.`
### Repository and Source-Control Flow ### Repository and Source-Control Flow
@ -78,31 +82,47 @@ Atlas is intended to support:
Only components verified during implementation should be recorded as installed. Only components verified during implementation should be recorded as installed.
### Intended PlotDirector Deployment Topology ### PlotDirector Development Deployment Topology
PlotDirector deployment topology is not yet established. PlotDirector is deployed for development from the `onboarding` branch.
Current assumptions: - Repository: `git@git.catherinelynwood.com:nick/PlotDirector.git`
- Source path: `/srv/repos/PlotDirector`
- Entry project: `PlotLine/PlotLine.csproj`
- Target framework: `net10.0`
- Output assembly: `PlotLine.dll`
- Published path: `/srv/apps/plotdirector-dev/current`
- Uploads path: `/srv/apps/plotdirector-dev/uploads`
- Runtime environment file: `/etc/plotdirector/plotdirector.env`
- Service account: `plotdirector`
- systemd service: `plotdirector-dev`
- Kestrel endpoint: `http://127.0.0.1:5050`
- nginx hostname: `dev.plotdirector.com`
- Development database: `PlotDirector_Development`
- SQL login: `plotdirector_dev`
- Source repositories should live under `/srv/repos`. Windows LAN clients can test the development site with this hosts-file entry:
- Build artefacts should be generated outside this infrastructure repository.
- Deployed applications should be separated from source repositories.
- SQL data should use `/sql` once SQL Server paths are confirmed.
- nginx may act as the reverse proxy if the application is exposed over HTTP or HTTPS.
- Docker may host one or more services if containerisation is adopted.
Specific PlotDirector service names, ports, deployment paths, database names, and rollback procedures are `TODO: Confirm during the relevant implementation phase.` ```text
192.168.10.10 dev.plotdirector.com
```
SMTP, Stripe, and OpenAI are intentionally unconfigured in the Atlas development runtime unless explicitly provided through protected runtime configuration.
### Role of nginx ### Role of nginx
nginx is planned as the likely local reverse proxy for HTTP and HTTPS applications. Its future responsibilities may include: nginx is installed from the standard Ubuntu 26.04 repositories and currently serves a temporary Atlas HTTP status site.
- Routing requests to application services. - Package version: nginx `1.28.3`
- TLS termination. - Service: `nginx`
- Static file serving where appropriate. - Site configuration: `/etc/nginx/sites-available/atlas-default.conf`
- Request size, timeout, and header policy. - Version-controlled source: `config/nginx/atlas-default.conf`
- Static site root: `/var/www/atlas`
- Health endpoint: `http://192.168.10.10/health`
- PlotDirector development proxy source: `config/nginx/plotdirector-dev.conf`
- PlotDirector development hostname: `dev.plotdirector.com`
nginx installation, site layout, TLS certificate handling, and exposure rules are `TODO: Confirm during the relevant implementation phase.` HTTPS and TLS certificates are not configured yet.
### Role of SQL Server ### Role of SQL Server
@ -137,6 +157,10 @@ The intended storage and responsibility model is:
- Source repositories: `/srv/repos` - Source repositories: `/srv/repos`
- Infrastructure repository: `/srv/repos/AI-Development-Server` - Infrastructure repository: `/srv/repos/AI-Development-Server`
- PlotDirector source repository: `/srv/repos/PlotDirector`
- PlotDirector published application: `/srv/apps/plotdirector-dev/current`
- PlotDirector runtime configuration: `/etc/plotdirector/plotdirector.env`
- PlotDirector uploads: `/srv/apps/plotdirector-dev/uploads`
- Installer logs: `/var/log/atlas/install/` - Installer logs: `/var/log/atlas/install/`
- Discovery and validation reports: `/var/log/atlas/reports/` - Discovery and validation reports: `/var/log/atlas/reports/`
- Future installer state and markers: `/var/lib/atlas/` - Future installer state and markers: `/var/lib/atlas/`
@ -159,6 +183,7 @@ Known trust boundaries:
- Repository content versus live secret-bearing configuration. - Repository content versus live secret-bearing configuration.
- SQL data on `/sql` versus source and scripts on `/srv`. - SQL data on `/sql` versus source and scripts on `/srv`.
- Future public-facing application traffic through nginx, if enabled. - Future public-facing application traffic through nginx, if enabled.
- PlotDirector Kestrel listens only on loopback and is reached through nginx.
Unresolved trust-boundary details: Unresolved trust-boundary details:
@ -184,9 +209,10 @@ flowchart TB
Scripts[Future operational scripts<br>scripts/] Scripts[Future operational scripts<br>scripts/]
Tests[Future smoke tests<br>tests/] Tests[Future smoke tests<br>tests/]
SQL[(SQL data volume<br>/sql)] SQL[(SQL data volume<br>/sql)]
Deploy[Future deployed applications] Deploy[PlotDirector dev deployment<br>/srv/apps/plotdirector-dev/current]
Nginx[Future nginx reverse proxy] Nginx[nginx reverse proxy<br>dev.plotdirector.com]
Docker[Future Docker runtime] Docker[Docker runtime]
SqlContainer[SQL Server container<br>atlas-sql-server]
end end
Repo --> Install Repo --> Install
@ -194,7 +220,8 @@ flowchart TB
Repo --> Scripts Repo --> Scripts
Repo --> Tests Repo --> Tests
Source --> Deploy Source --> Deploy
Deploy --> SQL Deploy --> SqlContainer
SqlContainer --> SQL
Nginx --> Deploy Nginx --> Deploy
Docker --> Deploy Docker --> Deploy
``` ```

View File

@ -10,6 +10,116 @@ This file records chronological Atlas build and infrastructure milestones. Do no
### Phase ### Phase
Phase 2 PlotDirector development deployment.
### Objective
Build and run the PlotDirector `onboarding` branch on Atlas behind nginx using the local SQL Server container.
### Changes
- Cloned PlotDirector at `/srv/repos/PlotDirector` from `git@git.catherinelynwood.com:nick/PlotDirector.git`.
- Confirmed branch `onboarding` tracks `origin/onboarding`.
- Built `PlotLine/PlotLine.csproj` targeting `net10.0`.
- Created runtime configuration at `/etc/plotdirector/plotdirector.env`.
- Created SQL database `PlotDirector_Development`.
- Created SQL login `plotdirector_dev`.
- Applied numbered SQL scripts `001` through `131`; `Stripe_Price_IDs_Prod.sql` was not run.
- Published the application to `/srv/apps/plotdirector-dev/current`.
- Added systemd unit template `config/systemd/plotdirector-dev.service`.
- Added nginx proxy configuration `config/nginx/plotdirector-dev.conf`.
- Added deployment phase `install/phases/50-plotdirector-dev.sh`.
### Validation
- `dotnet restore` passed.
- `dotnet build --configuration Release` passed with zero warnings.
- `dotnet publish --configuration Release` passed.
- `plotdirector-dev` systemd service started and listened on `127.0.0.1:5050`.
- nginx proxied `dev.plotdirector.com` to Kestrel.
- `/health` returned HTTP 200 from nginx.
- The development database contained the expected schema after script application.
### Problems
- SQL script execution required `sqlcmd -I` from script `076` onward so `QUOTED_IDENTIFIER` was enabled.
- `/srv` was `0700`, which prevented the non-root service account from traversing to `/srv/apps`. Atlas now grants execute-only traversal on `/srv`.
- The repository contains legacy hard-coded connection strings. They were not copied into Atlas runtime configuration.
### Decisions
- Run the application as the non-root `plotdirector` service account.
- Keep Kestrel bound to `127.0.0.1:5050`.
- Use nginx hostname `dev.plotdirector.com` for LAN development.
- Leave SMTP, Stripe, and OpenAI unconfigured for smoke testing.
### Git Commit
Not yet committed.
### Operator
Codex.
### Date
2026-07-10
### Phase
Phase 2 nginx and firewall baseline.
### Objective
Install nginx, deploy a minimal Atlas status site, restrict SQL Server binding, and enable a safe UFW baseline.
### Changes
- Added `install/phases/40-nginx.sh`.
- Installed nginx `1.28.3` from Ubuntu `resolute-updates`.
- Added Atlas default nginx site under `config/nginx/`.
- Deployed static site content to `/var/www/atlas`.
- Enabled `/health` endpoint.
- Enabled UFW with default deny incoming and default allow outgoing.
- Allowed inbound TCP `22`, `80`, and `443`.
- Allowed inbound TCP `1433` only from `192.168.10.0/24`.
- Changed SQL Server container binding to `192.168.10.10:1433:1433`.
### Validation
- `nginx -t` passed.
- HTTP checks passed for `127.0.0.1` and `192.168.10.10`.
- `/health` returned HTTP 200.
- nginx restart test passed.
- SQL Server remained healthy after container recreation.
- SQL data persisted after container recreation.
- SQL LAN connection test passed.
- Atlas discovery and validation passed.
### Problems
- Initial nginx deployment encountered a duplicate default-server config while the Ubuntu default site was still enabled. The installer now validates before and after disabling the default site.
### Decisions
- Keep nginx HTTP-only until a later TLS phase.
- Keep the temporary Atlas status site separate from future PlotDirector proxy configuration.
- Do not rely on UFW alone for Docker-published SQL Server; bind SQL Server to the Atlas LAN address.
### Git Commit
Not yet committed.
### Operator
Codex.
### Date
2026-07-10
### Phase
Phase 2 Docker and SQL Server container platform. Phase 2 Docker and SQL Server container platform.
### Objective ### Objective

View File

@ -11,6 +11,8 @@ This guide describes the planned rebuild path for Atlas. At Phase 1, the reposit
- .NET installation script: present. - .NET installation script: present.
- Docker installation script: present. - Docker installation script: present.
- SQL Server container installation script: present. - SQL Server container installation script: present.
- nginx installation script: present.
- PlotDirector development deployment 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.
@ -118,8 +120,14 @@ Planned bootstrap flow:
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 `install/phases/30-docker.sh`. 8. Run `install/phases/30-docker.sh`.
9. Run `install/phases/20-sql-server.sh` after Docker is available. 9. Run `install/phases/20-sql-server.sh` after Docker is available.
10. Run additional ordered installation scripts from `install/phases/` when they exist. 10. Run `install/phases/40-nginx.sh`.
11. Run smoke tests from `tests/` when they exist. 11. Apply the UFW baseline rules.
12. Clone PlotDirector to `/srv/repos/PlotDirector` on branch `onboarding`.
13. Create protected PlotDirector runtime configuration at `/etc/plotdirector/plotdirector.env`.
14. Initialise the `PlotDirector_Development` database using the numbered SQL scripts.
15. Run `install/phases/50-plotdirector-dev.sh`.
16. Run additional ordered installation scripts from `install/phases/` when they exist.
17. 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.`
@ -132,6 +140,7 @@ install/phases/10-dotnet.sh
install/phases/20-sql-server.sh install/phases/20-sql-server.sh
install/phases/30-docker.sh install/phases/30-docker.sh
install/phases/40-nginx.sh install/phases/40-nginx.sh
install/phases/50-plotdirector-dev.sh
``` ```
Current Docker and SQL Server commands: Current Docker and SQL Server commands:
@ -154,7 +163,57 @@ 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. 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: Current nginx validation commands:
```bash
sudo nginx -t
systemctl is-active nginx
curl -fsS http://127.0.0.1/health
curl -fsS http://192.168.10.10/health
```
Current firewall inspection command:
```bash
sudo ufw status numbered
```
Current PlotDirector development deployment command:
```bash
install/phases/50-plotdirector-dev.sh
```
PlotDirector runtime details:
- Source repository: `/srv/repos/PlotDirector`
- Required branch: `onboarding`
- Entry project: `PlotLine/PlotLine.csproj`
- Published path: `/srv/apps/plotdirector-dev/current`
- Runtime environment file: `/etc/plotdirector/plotdirector.env`
- Service: `plotdirector-dev`
- Kestrel endpoint: `http://127.0.0.1:5050`
- nginx hostname: `dev.plotdirector.com`
- Database: `PlotDirector_Development`
- SQL login: `plotdirector_dev`
Use this Windows hosts-file entry for LAN testing:
```text
192.168.10.10 dev.plotdirector.com
```
PlotDirector validation commands:
```bash
systemctl is-active plotdirector-dev
journalctl -u plotdirector-dev --no-pager -n 100
curl --noproxy '*' -fsS http://127.0.0.1:5050/ >/dev/null
curl --noproxy '*' -fsS -H 'Host: dev.plotdirector.com' http://127.0.0.1/ >/dev/null
sudo nginx -t
```
Installation scripts must:
- Be idempotent. - Be idempotent.
- Validate prerequisites. - Validate prerequisites.

View File

@ -54,6 +54,8 @@ Current Phase 2 installer:
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-docker.sh install/phases/30-docker.sh
install/phases/40-nginx.sh
install/phases/50-plotdirector-dev.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`.
@ -67,11 +69,19 @@ Current installed SDKs:
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. 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.
nginx is installed from Ubuntu repositories and serves the temporary Atlas status site until PlotDirector routing is configured.
PlotDirector development deployment uses `/srv/repos/PlotDirector` on branch `onboarding`, publishes `PlotLine.dll` to `/srv/apps/plotdirector-dev/current`, runs as the `plotdirector` service account, and is proxied by nginx as `dev.plotdirector.com`.
The PlotDirector runtime environment file is `/etc/plotdirector/plotdirector.env`. It contains secrets and must never be committed.
Runtime output belongs outside the repository: Runtime output belongs outside the repository:
- `/var/log/atlas/install/` - `/var/log/atlas/install/`
- `/var/log/atlas/reports/` - `/var/log/atlas/reports/`
- `/var/lib/atlas/` - `/var/lib/atlas/`
- `/srv/apps/plotdirector-dev/current`
- `/srv/apps/plotdirector-dev/uploads`
## Security Considerations ## Security Considerations

167
install/phases/40-nginx.sh Executable file
View File

@ -0,0 +1,167 @@
#!/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"
# shellcheck source=install/lib/packages.sh
source "${INSTALL_DIR}/lib/packages.sh"
atlas_set_script_name "40-nginx.sh"
atlas_install_error_trap
ATLAS_SITE_ROOT="/var/www/atlas"
ATLAS_SITE_CONF_SOURCE="${REPO_ROOT}/config/nginx/atlas-default.conf"
ATLAS_SITE_CONF_DEST="/etc/nginx/sites-available/atlas-default.conf"
ATLAS_SITE_LINK="/etc/nginx/sites-enabled/atlas-default.conf"
NGINX_DEFAULT_LINK="/etc/nginx/sites-enabled/default"
NGINX_BACKUP_DIR="/etc/nginx/atlas-backups"
usage() {
cat <<'USAGE'
Usage: install/phases/40-nginx.sh [options]
Install nginx from Ubuntu repositories and deploy the temporary Atlas default
HTTP status site. This does not configure HTTPS or PlotDirector proxying.
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
install_nginx() {
if atlas_package_installed nginx; then
atlas_log_ok "nginx is already installed"
else
atlas_install_packages nginx
fi
}
write_site_content() {
local tmp_file
tmp_file="$(mktemp)"
cat >"$tmp_file" <<'HTML'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Atlas</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; background: #f6f7f9; color: #1f2933; }
main { max-width: 42rem; margin: 12vh auto; padding: 2rem; }
h1 { font-size: 2rem; margin: 0 0 1rem; }
p { line-height: 1.5; }
</style>
</head>
<body>
<main>
<h1>Atlas is operational</h1>
<p>This is the temporary Atlas status site. Application routing has not been configured yet.</p>
</main>
</body>
</html>
HTML
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: would deploy static site content to ${ATLAS_SITE_ROOT}"
rm -f "$tmp_file"
return 0
fi
sudo -n install -d -m 0755 -o www-data -g www-data "$ATLAS_SITE_ROOT"
sudo -n install -m 0644 -o www-data -g www-data "$tmp_file" "${ATLAS_SITE_ROOT}/index.html"
rm -f "$tmp_file"
}
deploy_nginx_config() {
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: would deploy ${ATLAS_SITE_CONF_DEST}"
atlas_log_info "Dry-run mode: would enable Atlas nginx site and disable Ubuntu default site"
return 0
fi
sudo -n install -d -m 0755 /etc/nginx/sites-available /etc/nginx/sites-enabled
sudo -n install -d -m 0750 "$NGINX_BACKUP_DIR"
sudo -n nginx -t
if [[ -e "$ATLAS_SITE_CONF_DEST" ]] && ! cmp -s "$ATLAS_SITE_CONF_SOURCE" "$ATLAS_SITE_CONF_DEST"; then
sudo -n cp -a "$ATLAS_SITE_CONF_DEST" "${NGINX_BACKUP_DIR}/atlas-default.conf.$(date -u '+%Y%m%dT%H%M%SZ').bak"
fi
sudo -n install -m 0644 "$ATLAS_SITE_CONF_SOURCE" "$ATLAS_SITE_CONF_DEST"
if [[ -e "$NGINX_DEFAULT_LINK" ]]; then
sudo -n cp -a "$NGINX_DEFAULT_LINK" "${NGINX_BACKUP_DIR}/default-link.$(date -u '+%Y%m%dT%H%M%SZ').bak"
sudo -n rm -f "$NGINX_DEFAULT_LINK"
fi
sudo -n ln -sfn "$ATLAS_SITE_CONF_DEST" "$ATLAS_SITE_LINK"
sudo -n nginx -t
}
enable_nginx() {
atlas_sudo_run systemctl enable nginx
atlas_sudo_run systemctl restart nginx
}
verify_nginx() {
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: would verify nginx service and health endpoint"
return 0
fi
sudo -n nginx -t
systemctl is-enabled nginx >/dev/null
systemctl is-active nginx >/dev/null
curl -fsS http://127.0.0.1/health >/dev/null
curl -fsS http://192.168.10.10/health >/dev/null
atlas_log_ok "nginx verification passed"
}
main() {
atlas_require_sudo_noninteractive || {
atlas_log_error "Passwordless non-interactive sudo is required"
exit "${ATLAS_EXIT_PREREQ}"
}
install_nginx
write_site_content
deploy_nginx_config
enable_nginx
verify_nginx
}
main

View File

@ -0,0 +1,218 @@
#!/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"
# shellcheck source=install/lib/files.sh
source "${INSTALL_DIR}/lib/files.sh"
atlas_set_script_name "50-plotdirector-dev.sh"
atlas_install_error_trap
APP_REPO="/srv/repos/PlotDirector"
APP_BRANCH="onboarding"
APP_PROJECT="${APP_REPO}/PlotLine/PlotLine.csproj"
PUBLISH_DIR="/srv/apps/plotdirector-dev/current"
UPLOADS_DIR="/srv/apps/plotdirector-dev/uploads"
ENV_FILE="/etc/plotdirector/plotdirector.env"
SERVICE_USER="plotdirector"
SERVICE_NAME="plotdirector-dev"
SYSTEMD_SOURCE="${REPO_ROOT}/config/systemd/plotdirector-dev.service"
SYSTEMD_DEST="/etc/systemd/system/plotdirector-dev.service"
NGINX_SOURCE="${REPO_ROOT}/config/nginx/plotdirector-dev.conf"
NGINX_DEST="/etc/nginx/sites-available/plotdirector-dev.conf"
NGINX_LINK="/etc/nginx/sites-enabled/plotdirector-dev.conf"
NGINX_BACKUP_DIR="/etc/nginx/atlas-backups"
ALLOW_DIRTY=0
usage() {
cat <<'USAGE'
Usage: install/phases/50-plotdirector-dev.sh [options]
Publish and run the PlotDirector onboarding branch as the Atlas development
instance behind nginx. This script does not modify PlotDirector source,
create Git commits, or store secrets.
Prerequisites:
- /srv/repos/PlotDirector exists on branch onboarding.
- /etc/plotdirector/plotdirector.env exists and contains runtime settings.
- The PlotDirector development database has already been initialised.
Options:
--help Show this help text.
--version Show script/framework version.
--dry-run Show intended actions without making changes.
--verbose Print additional diagnostic output.
--allow-dirty Permit deployment from a dirty PlotDirector working tree.
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
;;
--allow-dirty)
ALLOW_DIRTY=1
;;
*)
atlas_usage_error "Unknown option: $1"
exit "${ATLAS_EXIT_USAGE}"
;;
esac
shift
done
require_prerequisites() {
atlas_require_command git
atlas_require_command dotnet
atlas_require_command curl
atlas_require_sudo_noninteractive || {
atlas_log_error "Passwordless non-interactive sudo is required"
exit "${ATLAS_EXIT_PREREQ}"
}
if [[ ! -d "${APP_REPO}/.git" ]]; then
atlas_log_error "PlotDirector repository not found at ${APP_REPO}"
exit "${ATLAS_EXIT_PREREQ}"
fi
local current_branch
current_branch="$(git -C "$APP_REPO" branch --show-current)"
if [[ "$current_branch" != "$APP_BRANCH" ]]; then
atlas_log_error "Expected ${APP_REPO} to be on ${APP_BRANCH}; found ${current_branch}"
exit "${ATLAS_EXIT_UNSAFE}"
fi
if [[ "$ALLOW_DIRTY" != "1" ]] && [[ -n "$(git -C "$APP_REPO" status --porcelain)" ]]; then
atlas_log_error "PlotDirector working tree is dirty. Use --allow-dirty only for deliberate local deployments."
exit "${ATLAS_EXIT_UNSAFE}"
fi
if ! sudo -n test -f "$ENV_FILE"; then
atlas_log_error "Runtime environment file is missing: ${ENV_FILE}"
exit "${ATLAS_EXIT_PREREQ}"
fi
}
ensure_runtime_user_and_dirs() {
if atlas_is_dry_run; then
atlas_log_info "Dry-run mode: would ensure ${SERVICE_USER} user and deployment directories"
return 0
fi
if ! id "$SERVICE_USER" >/dev/null 2>&1; then
sudo -n useradd --system --home-dir /srv/apps/plotdirector-dev --shell /usr/sbin/nologin --create-home "$SERVICE_USER"
fi
sudo -n install -d -m 0750 -o "$SERVICE_USER" -g "$SERVICE_USER" /srv/apps/plotdirector-dev "$PUBLISH_DIR" "$UPLOADS_DIR"
local srv_mode
srv_mode="$(stat -c '%a' /srv)"
if (( (8#$srv_mode & 0001) == 0 )); then
sudo -n chmod o+x /srv
fi
sudo -n chown root:"$SERVICE_USER" /etc/plotdirector
sudo -n chmod 0750 /etc/plotdirector
sudo -n chown root:"$SERVICE_USER" "$ENV_FILE"
sudo -n chmod 0640 "$ENV_FILE"
}
publish_application() {
if atlas_is_dry_run; then
atlas_log_info "Dry-run mode: would restore and publish ${APP_PROJECT} to ${PUBLISH_DIR}"
return 0
fi
local publish_tmp
publish_tmp="$(mktemp -d)"
dotnet restore "$APP_PROJECT"
dotnet publish "$APP_PROJECT" --configuration Release --output "$publish_tmp"
sudo -n rsync -a --delete "${publish_tmp}/" "${PUBLISH_DIR}/"
sudo -n chown -R "${SERVICE_USER}:${SERVICE_USER}" "$PUBLISH_DIR" "$UPLOADS_DIR"
sudo -n find "$PUBLISH_DIR" -type d -exec chmod 0750 {} +
sudo -n find "$PUBLISH_DIR" -type f -exec chmod 0640 {} +
rm -rf "$publish_tmp"
}
deploy_systemd_unit() {
if atlas_is_dry_run; then
atlas_log_info "Dry-run mode: would deploy ${SYSTEMD_DEST}"
atlas_log_info "Dry-run mode: would enable ${SERVICE_NAME}"
return 0
fi
if [[ -f "$SYSTEMD_DEST" ]] && cmp -s "$SYSTEMD_SOURCE" "$SYSTEMD_DEST"; then
atlas_log_ok "No change required for ${SYSTEMD_DEST}"
else
sudo -n install -m 0644 -o root -g root "$SYSTEMD_SOURCE" "$SYSTEMD_DEST"
atlas_log_ok "Updated ${SYSTEMD_DEST}"
fi
sudo -n systemctl daemon-reload
sudo -n systemctl enable "$SERVICE_NAME"
}
deploy_nginx_site() {
if atlas_is_dry_run; then
atlas_log_info "Dry-run mode: would deploy nginx site ${NGINX_DEST}"
return 0
fi
sudo -n install -d -m 0755 /etc/nginx/sites-available /etc/nginx/sites-enabled
sudo -n install -d -m 0750 "$NGINX_BACKUP_DIR"
sudo -n nginx -t
sudo -n install -m 0644 "$NGINX_SOURCE" "$NGINX_DEST"
sudo -n ln -sfn "$NGINX_DEST" "$NGINX_LINK"
sudo -n nginx -t
}
restart_and_verify() {
if atlas_is_dry_run; then
atlas_log_info "Dry-run mode: would restart ${SERVICE_NAME}, reload nginx, and verify HTTP responses"
return 0
fi
sudo -n systemctl restart "$SERVICE_NAME"
local attempt
for attempt in {1..30}; do
if curl -fsS http://127.0.0.1:5050/ >/dev/null 2>&1; then
break
fi
sleep 2
done
curl -fsS http://127.0.0.1:5050/ >/dev/null
sudo -n nginx -t
sudo -n systemctl reload nginx
curl -fsS -H "Host: dev.plotdirector.com" http://127.0.0.1/health >/dev/null
atlas_log_ok "PlotDirector development deployment verified"
}
main() {
require_prerequisites
ensure_runtime_user_and_dirs
publish_application
deploy_systemd_unit
deploy_nginx_site
restart_and_verify
}
main

View File

@ -23,6 +23,8 @@ Each phase must source the shared framework under `install/lib/`, support dry-ru
- `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. - `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. - `30-docker.sh`: installs Docker Engine from Docker's official Ubuntu APT repository.
- `40-nginx.sh`: installs nginx and deploys the temporary Atlas status site.
- `50-plotdirector-dev.sh`: publishes and restarts the PlotDirector development instance from `/srv/repos/PlotDirector`.
Installed SDKs after the current Phase 2 run: Installed SDKs after the current Phase 2 run:
@ -35,3 +37,15 @@ Current SQL Server deployment:
- Digest: `sha256:698682bab57c02c42bc0aa274b158aeb242d8e9104149a7489628d5535805816` - Digest: `sha256:698682bab57c02c42bc0aa274b158aeb242d8e9104149a7489628d5535805816`
- Container: `atlas-sql-server` - Container: `atlas-sql-server`
- Compose project: `atlas-sql` - Compose project: `atlas-sql`
Current PlotDirector development deployment:
- Repository: `/srv/repos/PlotDirector`
- Branch: `onboarding`
- Entry project: `PlotLine/PlotLine.csproj`
- Target framework: `net10.0`
- Assembly: `PlotLine.dll`
- Published path: `/srv/apps/plotdirector-dev/current`
- Service: `plotdirector-dev`
- nginx hostname: `dev.plotdirector.com`
- Database: `PlotDirector_Development`

View File

@ -53,3 +53,4 @@ Additional areas may be added when a repeated operational need exists.
- 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. - SQL helper scripts must not embed or print SQL administrator passwords.
- Firewall inspection should use `sudo ufw status numbered`.

View File

@ -31,4 +31,3 @@ fi
exec sudo -n docker exec -i "$CONTAINER_NAME" /bin/bash -lc \ 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 "$@"' \ 'SQLCMDPASSWORD="$MSSQL_SA_PASSWORD" exec /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -C "$@"' \
sqlcmd "$@" sqlcmd "$@"

View File

@ -131,6 +131,16 @@ sql_container_health() {
fi fi
} }
nginx_status() {
if ! have nginx; then
printf 'not installed\n'
elif systemctl is-active --quiet nginx 2>/dev/null && sudo -n nginx -t >/dev/null 2>&1; then
printf 'active-config-valid\n'
else
printf 'installed-needs-attention\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
@ -243,6 +253,7 @@ human_report() {
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 ' Docker status: %s\n' "$(docker_status)"
printf ' SQL Server container health: %s\n\n' "$(sql_container_health)" printf ' SQL Server container health: %s\n\n' "$(sql_container_health)"
printf ' nginx status: %s\n\n' "$(nginx_status)"
printf 'Services\n' printf 'Services\n'
printf ' Failed systemd services:\n' printf ' Failed systemd services:\n'
@ -305,6 +316,7 @@ json_report() {
--arg docker_version "$(component_version docker --version)" \ --arg docker_version "$(component_version docker --version)" \
--arg docker_status "$(docker_status)" \ --arg docker_status "$(docker_status)" \
--arg sql_container_health "$(sql_container_health)" \ --arg sql_container_health "$(sql_container_health)" \
--arg nginx_status "$(nginx_status)" \
--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)" \
@ -333,7 +345,8 @@ json_report() {
nginx: $nginx_version, nginx: $nginx_version,
docker: $docker_version, docker: $docker_version,
docker_status: $docker_status, docker_status: $docker_status,
sql_server_container_health: $sql_container_health sql_server_container_health: $sql_container_health,
nginx_status: $nginx_status
}, },
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

@ -246,6 +246,37 @@ check_docker_sql_state() {
fi fi
} }
check_nginx_ufw_state() {
if command -v nginx >/dev/null 2>&1; then
systemctl is-active --quiet nginx && atlas_validation_result PASS "nginx service is active" || atlas_validation_result FAIL "nginx service is not active"
sudo -n nginx -t >/dev/null 2>&1 && atlas_validation_result PASS "nginx configuration is valid" || atlas_validation_result FAIL "nginx configuration is invalid"
curl -fsS http://127.0.0.1/health >/dev/null 2>&1 && atlas_validation_result PASS "Atlas nginx health endpoint is reachable" || atlas_validation_result FAIL "Atlas nginx health endpoint is not reachable"
else
atlas_validation_result SKIPPED "nginx is not installed"
fi
if command -v ufw >/dev/null 2>&1 && sudo -n ufw status | grep -q '^Status: active'; then
atlas_validation_result PASS "UFW is active"
sudo -n ufw status numbered | grep -q '22/tcp' && atlas_validation_result PASS "UFW allows SSH" || atlas_validation_result FAIL "UFW SSH rule missing"
sudo -n ufw status numbered | grep -q '80/tcp' && atlas_validation_result PASS "UFW allows HTTP" || atlas_validation_result FAIL "UFW HTTP rule missing"
sudo -n ufw status numbered | grep -q '443/tcp' && atlas_validation_result PASS "UFW allows HTTPS" || atlas_validation_result FAIL "UFW HTTPS rule missing"
sudo -n ufw status numbered | grep -q '1433/tcp.*192.168.10.0/24' && atlas_validation_result PASS "UFW restricts SQL Server to LAN subnet" || atlas_validation_result FAIL "UFW SQL LAN rule missing"
else
atlas_validation_result FAIL "UFW is not active"
fi
if ss -ltn | awk '{print $4}' | grep -qx '192.168.10.10:1433'; then
atlas_validation_result PASS "SQL Server is bound to Atlas LAN address"
else
atlas_validation_result FAIL "SQL Server is not bound to Atlas LAN address"
fi
if ss -ltn | awk '{print $4}' | grep -Eq '(^0\.0\.0\.0:1433$|^\[::\]:1433$)'; then
atlas_validation_result FAIL "SQL Server is bound to all interfaces"
else
atlas_validation_result PASS "SQL Server is not bound to all interfaces"
fi
}
atlas_validation_reset atlas_validation_reset
printf 'Atlas Host Validation\n' printf 'Atlas Host Validation\n'
@ -290,6 +321,7 @@ check_free_space /sql "$MIN_SQL_FREE_GIB"
check_failed_services check_failed_services
check_docker_sql_state check_docker_sql_state
check_nginx_ufw_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"