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
+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;