Prepare isolated VPS UAT deployment bundle

This commit is contained in:
b0txec
2026-08-20 19:22:03 +03:00
parent bd053b073c
commit 52922b8e7f
14 changed files with 379 additions and 17 deletions
+4
View File
@@ -28,6 +28,10 @@ logs/
.DS_Store
.env
.env-fly
deploy/vps/.env.staging
deploy/vps/config/authelia/users_database.yml
deploy/vps/secrets/
deploy/vps/data/
data/*
!project/assembly.sbt
!src/main/scala/data
+32
View File
@@ -0,0 +1,32 @@
FROM node:22.14.0-alpine AS web-build
WORKDIR /build/web
COPY web/package.json web/package-lock.json ./
RUN npm ci
COPY web/ ./
RUN npm run build -- --mode production
FROM hseeberger/scala-sbt:17.0.2_1.8.2_2.13.10 AS scala-build
WORKDIR /build
COPY build.sbt ./
COPY project/ ./project/
COPY src/ ./src/
RUN sbt clean assembly
FROM amazoncorretto:17-alpine
RUN addgroup -S weather && adduser -S -G weather weather
WORKDIR /app
RUN mkdir -p /app/data/tmp /app/data/grids /app/web/dist && chown -R weather:weather /app
COPY --from=scala-build --chown=weather:weather /build/target/scala-2.13/app.jar /app/app.jar
COPY --from=web-build --chown=weather:weather /build/web/dist/ /app/web/dist/
USER weather
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1:8080/ || exit 1
CMD ["java", "-Xms128m", "-Xmx512m", "-XX:+UseG1GC", "-jar", "/app/app.jar"]
+53
View File
@@ -0,0 +1,53 @@
# WeatherTool VPS deployment bundle
This directory contains the reviewed templates for the temporary UAT deployment.
It is intentionally separate from the Rocky development Compose project.
## Boundaries
- Compose project: `weathertool-uat`
- WeatherTool host binding: `127.0.0.1:8002`
- Authelia host binding: `127.0.0.1:9091`
- PostgreSQL: private Docker network only; no host port
- Scheduled provider jobs: disabled
- Existing VPS applications, networks, volumes, and Nginx sites: untouched
The committed configuration contains no live password, password hash, or
cryptographic secret. Runtime material belongs only under `/srv/weathertool`.
## Runtime layout
Copy the committed bundle into `/srv/weathertool`, then create these untracked
paths before the first start:
```text
/srv/weathertool/
.env.staging
compose.yml
config/authelia/configuration.yml
config/authelia/users_database.yml
data/app/
data/authelia/
data/postgres/
secrets/jwt_secret
secrets/session_secret
secrets/storage_encryption_key
```
The Nginx files remain examples until the domain, Cloudflare origin certificate,
trusted client-address configuration, and private upstream tests are complete.
Do not enable the virtual host merely because the containers start successfully.
## Release image
Build the exact committed revision on Rocky:
```bash
git archive HEAD | docker build \
-f deploy/vps/Dockerfile \
-t "weathertool:$(git rev-parse HEAD)" -
```
Export the image, calculate its SHA-256 checksum, transfer both files, verify the
checksum on the VPS, and only then load it. The detailed operator sequence is in
`docs/VPS_STAGING_PLAN.md`.
+70
View File
@@ -0,0 +1,70 @@
name: weathertool-uat
services:
postgres:
image: postgres:16.1
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB in .env.staging}
POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env.staging}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env.staging}
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- backend
security_opt:
- no-new-privileges:true
app:
image: ${WEATHERTOOL_IMAGE:?set WEATHERTOOL_IMAGE in .env.staging}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DB_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB}
DB_USER: ${POSTGRES_USER}
DB_PASSWORD: ${POSTGRES_PASSWORD}
ENABLE_SCHEDULED_JOBS: "false"
DEBUG: "false"
ports:
- "127.0.0.1:${WEATHERTOOL_PORT:-8002}:8080"
volumes:
- ./data/app:/app/data
read_only: true
tmpfs:
- /tmp:size=64m,mode=1777
networks:
- backend
security_opt:
- no-new-privileges:true
authelia:
image: authelia/authelia:4.39.20
restart: unless-stopped
environment:
AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: /run/secrets/jwt_secret
AUTHELIA_SESSION_SECRET_FILE: /run/secrets/session_secret
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: /run/secrets/storage_encryption_key
ports:
- "127.0.0.1:${AUTHELIA_PORT:-9091}:9091"
volumes:
- ./config/authelia:/config:ro
- ./data/authelia:/var/lib/authelia
- ./secrets/jwt_secret:/run/secrets/jwt_secret:ro
- ./secrets/session_secret:/run/secrets/session_secret:ro
- ./secrets/storage_encryption_key:/run/secrets/storage_encryption_key:ro
networks:
- backend
security_opt:
- no-new-privileges:true
networks:
backend:
driver: bridge
internal: true
@@ -0,0 +1,64 @@
server:
address: tcp4://0.0.0.0:9091
endpoints:
authz:
auth-request:
implementation: AuthRequest
log:
level: info
format: text
identity_validation:
reset_password:
disable: true
authentication_backend:
password_reset:
disable: true
refresh_interval: 5m
file:
path: /config/users_database.yml
watch: true
password:
algorithm: argon2
argon2:
variant: argon2id
iterations: 3
memory: 65536
parallelism: 4
key_length: 32
salt_length: 16
access_control:
default_policy: deny
rules:
- domain: laikapstak.li
policy: one_factor
session:
name: weathertool_session
cookies:
- domain: laikapstak.li
authelia_url: https://auth.laikapstak.li
default_redirection_url: https://laikapstak.li
same_site: lax
inactivity: 1h
expiration: 12h
remember_me: -1
regulation:
modes:
- ip
max_retries: 3
find_time: 2m
ban_time: 15m
storage:
local:
path: /var/lib/authelia/db.sqlite3
notifier:
disable_startup_check: false
filesystem:
filename: /var/lib/authelia/notification.txt
@@ -0,0 +1,11 @@
# Generate the password hash on the VPS; never place a plaintext password here.
# Then copy this file to users_database.yml, replace the placeholders, and set
# mode 0640. The real users_database.yml is excluded from Git.
users:
newsroom:
disabled: false
displayname: Newsroom tester
password: REPLACE_WITH_ARGON2ID_HASH
email: newsroom@invalid.local
groups:
- testers
+8
View File
@@ -0,0 +1,8 @@
# Copy to /srv/weathertool/.env.staging and set mode 0600.
# Do not commit the real file.
WEATHERTOOL_IMAGE=weathertool:REPLACE_WITH_FULL_GIT_SHA
WEATHERTOOL_PORT=8002
AUTHELIA_PORT=9091
POSTGRES_DB=weather
POSTGRES_USER=weather
POSTGRES_PASSWORD=REPLACE_WITH_A_LONG_RANDOM_VALUE
@@ -0,0 +1,45 @@
# Do not enable this file until DNS, origin certificates, and private upstream
# checks are complete. Adapt certificate/include paths to the VPS conventions.
server {
listen 80;
listen [::]:80;
server_name laikapstak.li auth.laikapstak.li;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name auth.laikapstak.li;
ssl_certificate /etc/nginx/tls/laikapstak.li/origin.pem;
ssl_certificate_key /etc/nginx/tls/laikapstak.li/origin.key;
location / {
include /etc/nginx/snippets/weathertool-proxy.conf;
proxy_pass http://127.0.0.1:9091;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name laikapstak.li;
ssl_certificate /etc/nginx/tls/laikapstak.li/origin.pem;
ssl_certificate_key /etc/nginx/tls/laikapstak.li/origin.key;
# Start short during UAT. Increase only after TLS behavior is proven.
add_header Strict-Transport-Security "max-age=3600" always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy no-referrer always;
include /etc/nginx/snippets/weathertool-authelia-location.conf;
location / {
include /etc/nginx/snippets/weathertool-proxy.conf;
include /etc/nginx/snippets/weathertool-authelia-authrequest.conf;
proxy_pass http://127.0.0.1:8002;
}
}
@@ -0,0 +1,14 @@
auth_request /internal/authelia/authz;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
auth_request_set $redirection_url $upstream_http_location;
proxy_set_header Remote-User $user;
proxy_set_header Remote-Groups $groups;
proxy_set_header Remote-Name $name;
proxy_set_header Remote-Email $email;
error_page 401 =302 $redirection_url;
@@ -0,0 +1,25 @@
set $upstream_authelia http://127.0.0.1:9091/api/authz/auth-request;
location /internal/authelia/authz {
internal;
proxy_pass $upstream_authelia;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Original-URL $scheme://$host$request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Content-Length "";
proxy_set_header Connection "";
proxy_pass_request_body off;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_redirect http:// $scheme://;
proxy_http_version 1.1;
proxy_cache_bypass $cookie_weathertool_session;
proxy_no_cache $cookie_weathertool_session;
proxy_buffers 4 32k;
client_body_buffer_size 128k;
send_timeout 5m;
proxy_read_timeout 240s;
proxy_send_timeout 240s;
proxy_connect_timeout 240s;
}
@@ -0,0 +1,12 @@
proxy_set_header Host $host;
proxy_set_header X-Original-URL $scheme://$host$request_uri;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-URI $request_uri;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_http_version 1.1;
proxy_connect_timeout 15s;
proxy_read_timeout 240s;
proxy_send_timeout 240s;
+6 -1
View File
@@ -6,7 +6,7 @@
|---|---|---|
| Windows | Source editing and Git workflow only | Not applicable |
| Rocky Linux | Docker development and production-like staging | `http://192.168.1.101:9190` |
| Temporary VPS | Proposed month-long user-acceptance staging behind Cloudflare, Nginx, and Authelia | Not provisioned; hostname pending |
| Temporary VPS | Month-long user-acceptance staging behind Cloudflare, Nginx, and Authelia | `laikapstak.li` pending domain delivery and activation; services not started |
| Workplace production | Out of scope until reviewed and approved | Not documented here |
Rocky checkout:
@@ -124,3 +124,8 @@ Windows source and Git -> Rocky build and verification -> immutable image + chec
```
Releases are manual during user acceptance and are identified by the source commit SHA. The VPS keeps WeatherTool in its own `/srv/weathertool` tree, publishes the application only to a loopback port, and relies on Nginx plus Authelia at the public boundary. See `VPS_STAGING_PLAN.md` before provisioning or changing the VPS.
The committed templates live under `deploy/vps/`. They are not a second local
development environment. Validate them on Rocky, build the exact Git revision,
and transfer a checksummed image plus the minimal deployment files. Operational
environment files, password hashes, and Authelia secrets must remain VPS-only.
+2 -2
View File
@@ -13,7 +13,7 @@ This directory contains the working documentation for the WeatherTool modernizat
- Faktiskā symbol placement is automatic after manual image selection and is anchored to each rendered temperature badge.
- The first **Ūdens temperatūra** workspace is implemented with manual Latvian-named fields and required 1920×1080 and 3840×1440 exports; visual calibration remains in progress.
- Confirmed local Monda Regular/Bold files provide interface and generated-graphic typography; weather symbols use normalized transparent image assets.
- The next proposed environment is a temporary, isolated VPS user-acceptance deployment built on Rocky, protected by Cloudflare, Nginx, and removable Authelia authentication. It has not yet been provisioned.
- A version-controlled bundle is being prepared for a temporary, isolated VPS user-acceptance deployment built on Rocky and protected by Cloudflare, Nginx, and removable Authelia authentication. VPS directories exist, but no WeatherTool service has been started.
- This is not yet approved or hardened for workplace production.
## Documents
@@ -23,7 +23,7 @@ This directory contains the working documentation for the WeatherTool modernizat
- [Product workflows](PRODUCT_WORKFLOWS.md) — the intended purpose and current status of each visible workspace.
- [Update roadmap](UPDATE_ROADMAP.md) — phased technical, security, dependency, testing, and UI work.
- [Third-party notices](THIRD_PARTY_NOTICES.md) — licenses and attribution for adapted interface components.
- [Temporary VPS staging plan](VPS_STAGING_PLAN.md) — proposed isolation, authentication, release, backup, verification, and rollback model for external user testing.
- [Temporary VPS staging plan](VPS_STAGING_PLAN.md) — isolation, authentication, prepared deployment bundle, release, backup, verification, and rollback model for external user testing.
## Documentation rules
+33 -14
View File
@@ -1,6 +1,6 @@
# Temporary VPS staging plan
Status: **Proposed; WeatherTool has not been provisioned on the VPS**
Status: **Deployment bundle prepared; runtime directories created; services not started**
This document defines the next deployment stage for approximately one month of
newsroom-user testing. It records the intended boundary before any VPS changes
@@ -57,6 +57,10 @@ Temporary VPS
Cloudflare -> Nginx -> Authelia authorization -> WeatherTool
```
The host root plus `releases/` and `backups/` now exist with owner
`ubuntu:ubuntu` and mode `0750`. No Compose file, secret, image, database, or
running WeatherTool service has been placed there yet.
The VPS receives a built release artifact, not a development checkout. It gets
no credential or route for the Rocky Git repository, the private Forgejo
instance, or the home LAN.
@@ -91,9 +95,10 @@ WeatherTool will use:
- no shared Docker volumes, database, or application network with HOP or any
other service.
Only Nginx may expose WeatherTool publicly. PostgreSQL and Authelia's internal
service port must not be published to the internet. Docker port bindings must
not be allowed to bypass the intended firewall boundary.
Only Nginx may expose WeatherTool publicly. The committed bundle binds the app
to `127.0.0.1:8002`, binds the Authelia portal to `127.0.0.1:9091`, and gives
PostgreSQL no host port. Docker port bindings must not be allowed to bypass the
intended firewall boundary.
## Authentication boundary
@@ -123,7 +128,9 @@ include and Authelia service. The application remains unchanged.
## Domain, TLS, and Nginx
The WeatherTool hostname is not yet selected. Before DNS publication:
The selected application hostname is `laikapstak.li`; the authentication portal
will use `auth.laikapstak.li`. The OVH order and Cloudflare zone activation are
still pending. Before DNS publication:
1. Add an isolated Nginx virtual host without changing existing hosts.
2. Install a certificate valid for the exact WeatherTool/Authelia hostname or
@@ -138,9 +145,24 @@ The WeatherTool hostname is not yet selected. Before DNS publication:
9. Confirm TLS, static assets, login, logout, API calls, PNG downloads, real
client addresses, and direct-origin rejection.
Whether Authelia uses a dedicated `auth.<domain>` hostname or a subpath remains
an explicit deployment decision. A dedicated hostname is conventional; a
same-host subpath reduces DNS and certificate entries for this single app.
The dedicated authentication hostname is an explicit decision. Both hostnames
must be covered by the origin certificate and proxied through Cloudflare.
## Version-controlled deployment bundle
`deploy/vps/` now contains:
- a multi-stage production Dockerfile that builds both the Vite frontend and
Scala assembly, then runs as a non-root user;
- an isolated `weathertool-uat` Compose definition;
- an environment example with no operational secret;
- pinned Authelia `4.39.20` configuration and an example file-user database;
- official-style Nginx AuthRequest integration snippets; and
- a disabled Nginx virtual-host example for the app and auth portal.
The bundle is a template until it passes Compose rendering, Authelia validation,
image build, container health, and private loopback smoke tests. Nginx activation
and public DNS remain later checkpoints.
## Manual release procedure
@@ -202,13 +224,10 @@ Proposed temporary-staging minimum:
## Decisions and prerequisites still open
- Final public hostname and Cloudflare zone.
- Dedicated Authelia hostname versus same-host subpath.
- Confirmed VPS capacity and unused loopback port.
- Completion of the OVH order, nameserver delegation, and Cloudflare activation.
- Cloudflare origin certificate covering both selected hostnames.
- Deployment account and whether normal releases use public SSH or Tailscale.
- Final session and backup-retention values.
- Secure operator-side generation and delivery of the shared credential.
- A production Dockerfile/Compose definition validated independently from the
Rocky development Compose file.
- Validation of the prepared production Dockerfile/Compose bundle on Rocky.
- Health/readiness behavior suitable for automated release verification.