Prepare isolated VPS UAT deployment bundle
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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,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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user