fix(ops): enable HTTPS backoffice and Sentry via Finland relay
Ship TLS nginx for backoffice, keep HTTP bootstrap until certs exist, proxy /monitoring to sentry-relay, and tunnel server/edge Sentry through Finland.
This commit is contained in:
parent
d2222fe73e
commit
d3c48a1ed1
29
deploy/nginx/backoffice-http-bootstrap.conf
Normal file
29
deploy/nginx/backoffice-http-bootstrap.conf
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Admin backoffice — HTTP bootstrap until Let's Encrypt exists.
|
||||||
|
# After: certbot certonly --webroot -w /var/www/html -d backoffice.ghabilee.ir
|
||||||
|
# deploy switches to deploy/nginx/backoffice.conf (TLS).
|
||||||
|
|
||||||
|
upstream ghabilee_admin {
|
||||||
|
server 127.0.0.1:3009;
|
||||||
|
keepalive 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name backoffice.ghabilee.ir;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://ghabilee_admin;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
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_read_timeout 60s;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,19 @@
|
|||||||
# Admin backoffice — TLS terminated by Nginx, app on 127.0.0.1:3009
|
# Admin backoffice — TLS terminated by Nginx, app on 127.0.0.1:3009
|
||||||
# DNS: point backoffice.ghabilee.ir at this VPS, then:
|
#
|
||||||
# certbot --nginx -d backoffice.ghabilee.ir
|
# Requires:
|
||||||
|
# - DNS A for backoffice.ghabilee.ir → this VPS
|
||||||
map $http_upgrade $connection_upgrade {
|
# - certbot cert: /etc/letsencrypt/live/backoffice.ghabilee.ir/
|
||||||
default upgrade;
|
# - ghabilee site enabled (provides $connection_upgrade map)
|
||||||
'' close;
|
#
|
||||||
}
|
# Issue / renew:
|
||||||
|
# certbot certonly --webroot -w /var/www/html -d backoffice.ghabilee.ir
|
||||||
|
|
||||||
upstream ghabilee_admin {
|
upstream ghabilee_admin {
|
||||||
server 127.0.0.1:3009;
|
server 127.0.0.1:3009;
|
||||||
keepalive 16;
|
keepalive 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# HTTP → ACME + redirect to HTTPS
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
@ -21,6 +23,38 @@ server {
|
|||||||
root /var/www/html;
|
root /var/www/html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://backoffice.ghabilee.ir$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
server_name backoffice.ghabilee.ir;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/backoffice.ghabilee.ir/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/backoffice.ghabilee.ir/privkey.pem;
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||||
|
|
||||||
|
client_max_body_size 10m;
|
||||||
|
|
||||||
|
# Browser Sentry tunnel → Finland (ingest.de is blocked from Iran egress).
|
||||||
|
location = /monitoring {
|
||||||
|
proxy_pass https://sentry-relay.ghabilee.ir/monitoring;
|
||||||
|
proxy_ssl_server_name on;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host sentry-relay.ghabilee.ir;
|
||||||
|
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_connect_timeout 5s;
|
||||||
|
proxy_read_timeout 20s;
|
||||||
|
proxy_send_timeout 20s;
|
||||||
|
client_max_body_size 1m;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://ghabilee_admin;
|
proxy_pass http://ghabilee_admin;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
1. Point the DNS record for `backoffice.ghabilee.ir` to the production VPS.
|
1. Point the DNS record for `backoffice.ghabilee.ir` to the production VPS.
|
||||||
2. Create `/opt/ghabilee-admin/.env` from `.env.example` and set production values.
|
2. Create `/opt/ghabilee-admin/.env` from `.env.example` and set production values.
|
||||||
3. Configure the reverse proxy to forward `backoffice.ghabilee.ir` to `127.0.0.1:3009` and enable TLS.
|
3. Issue TLS: `certbot certonly --webroot -w /var/www/html -d backoffice.ghabilee.ir` (after ACME path is reachable on port 80).
|
||||||
4. Push to `main` (or run the deploy workflow manually) after the environment is ready.
|
4. Deploy installs `deploy/nginx/backoffice.conf` when the cert exists (otherwise HTTP bootstrap); app listens on `127.0.0.1:3009`.
|
||||||
|
5. Push to `main` (or run the deploy workflow manually) after the environment is ready.
|
||||||
|
|
||||||
Deploy Telegram alerts use admin-specific copy in `scripts/notify-deploy.sh`
|
Deploy Telegram alerts use admin-specific copy in `scripts/notify-deploy.sh`
|
||||||
(success → backoffice URL; distinct from the consumer frontend message).
|
(success → backoffice URL; distinct from the consumer frontend message).
|
||||||
|
|||||||
@ -109,8 +109,14 @@ scp -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=acce
|
|||||||
deploy/docker-compose.production.yml \
|
deploy/docker-compose.production.yml \
|
||||||
"${IRAN_USER}@${IRAN_HOST}:${IRAN_APP}/docker-compose.yml"
|
"${IRAN_USER}@${IRAN_HOST}:${IRAN_APP}/docker-compose.yml"
|
||||||
if [[ -f deploy/nginx/backoffice.conf ]]; then
|
if [[ -f deploy/nginx/backoffice.conf ]]; then
|
||||||
|
NGINX_BACKOFFICE=deploy/nginx/backoffice.conf
|
||||||
|
if ! "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" 'test -s /etc/letsencrypt/live/backoffice.ghabilee.ir/fullchain.pem'; then
|
||||||
|
if [[ -f deploy/nginx/backoffice-http-bootstrap.conf ]]; then
|
||||||
|
NGINX_BACKOFFICE=deploy/nginx/backoffice-http-bootstrap.conf
|
||||||
|
fi
|
||||||
|
fi
|
||||||
scp -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \
|
scp -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \
|
||||||
deploy/nginx/backoffice.conf \
|
"$NGINX_BACKOFFICE" \
|
||||||
"${IRAN_USER}@${IRAN_HOST}:/etc/nginx/sites-available/backoffice"
|
"${IRAN_USER}@${IRAN_HOST}:/etc/nginx/sites-available/backoffice"
|
||||||
"${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" '
|
"${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" '
|
||||||
ln -sfn /etc/nginx/sites-available/backoffice /etc/nginx/sites-enabled/backoffice
|
ln -sfn /etc/nginx/sites-available/backoffice /etc/nginx/sites-enabled/backoffice
|
||||||
|
|||||||
@ -21,7 +21,13 @@ fi
|
|||||||
|
|
||||||
install -m 0644 "$SRC_DIR/deploy/docker-compose.production.yml" "$APP_DIR/docker-compose.yml"
|
install -m 0644 "$SRC_DIR/deploy/docker-compose.production.yml" "$APP_DIR/docker-compose.yml"
|
||||||
if [ -f "$SRC_DIR/deploy/nginx/backoffice.conf" ]; then
|
if [ -f "$SRC_DIR/deploy/nginx/backoffice.conf" ]; then
|
||||||
|
if [ -s /etc/letsencrypt/live/backoffice.ghabilee.ir/fullchain.pem ]; then
|
||||||
install -m 0644 "$SRC_DIR/deploy/nginx/backoffice.conf" /etc/nginx/sites-available/backoffice
|
install -m 0644 "$SRC_DIR/deploy/nginx/backoffice.conf" /etc/nginx/sites-available/backoffice
|
||||||
|
elif [ -f "$SRC_DIR/deploy/nginx/backoffice-http-bootstrap.conf" ]; then
|
||||||
|
install -m 0644 "$SRC_DIR/deploy/nginx/backoffice-http-bootstrap.conf" /etc/nginx/sites-available/backoffice
|
||||||
|
else
|
||||||
|
install -m 0644 "$SRC_DIR/deploy/nginx/backoffice.conf" /etc/nginx/sites-available/backoffice
|
||||||
|
fi
|
||||||
ln -sfn /etc/nginx/sites-available/backoffice /etc/nginx/sites-enabled/backoffice
|
ln -sfn /etc/nginx/sites-available/backoffice /etc/nginx/sites-enabled/backoffice
|
||||||
nginx -t
|
nginx -t
|
||||||
systemctl reload nginx
|
systemctl reload nginx
|
||||||
|
|||||||
@ -4,6 +4,14 @@ import { shouldDropExpectedProductSentryEvent } from './lib/observability/expect
|
|||||||
|
|
||||||
const dsn = process.env.SENTRY_DSN?.trim() || process.env.NEXT_PUBLIC_SENTRY_DSN?.trim()
|
const dsn = process.env.SENTRY_DSN?.trim() || process.env.NEXT_PUBLIC_SENTRY_DSN?.trim()
|
||||||
|
|
||||||
|
/** Iran runtime cannot reach ingest.de; send via Finland relay. */
|
||||||
|
function sentryTunnel(): string | undefined {
|
||||||
|
const fromEnv = process.env.SENTRY_TUNNEL?.trim()
|
||||||
|
if (fromEnv) return fromEnv
|
||||||
|
if (process.env.NODE_ENV === 'production') return 'https://sentry-relay.ghabilee.ir/monitoring'
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
function tracesSampleRate(): number {
|
function tracesSampleRate(): number {
|
||||||
const raw = process.env.SENTRY_TRACES_SAMPLE_RATE ?? '0.1'
|
const raw = process.env.SENTRY_TRACES_SAMPLE_RATE ?? '0.1'
|
||||||
const parsed = Number.parseFloat(raw)
|
const parsed = Number.parseFloat(raw)
|
||||||
@ -17,6 +25,7 @@ export function initEdgeSentry() {
|
|||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn,
|
dsn,
|
||||||
|
tunnel: sentryTunnel(),
|
||||||
environment: process.env.SENTRY_ENVIRONMENT?.trim() || process.env.NODE_ENV || 'production',
|
environment: process.env.SENTRY_ENVIRONMENT?.trim() || process.env.NODE_ENV || 'production',
|
||||||
release: process.env.SENTRY_RELEASE?.trim() || process.env.NEXT_PUBLIC_APP_VERSION || undefined,
|
release: process.env.SENTRY_RELEASE?.trim() || process.env.NEXT_PUBLIC_APP_VERSION || undefined,
|
||||||
tracesSampleRate: tracesSampleRate(),
|
tracesSampleRate: tracesSampleRate(),
|
||||||
|
|||||||
@ -4,6 +4,14 @@ import { shouldDropExpectedProductSentryEvent } from './lib/observability/expect
|
|||||||
|
|
||||||
const dsn = process.env.SENTRY_DSN?.trim() || process.env.NEXT_PUBLIC_SENTRY_DSN?.trim()
|
const dsn = process.env.SENTRY_DSN?.trim() || process.env.NEXT_PUBLIC_SENTRY_DSN?.trim()
|
||||||
|
|
||||||
|
/** Iran runtime cannot reach ingest.de; send via Finland relay. */
|
||||||
|
function sentryTunnel(): string | undefined {
|
||||||
|
const fromEnv = process.env.SENTRY_TUNNEL?.trim()
|
||||||
|
if (fromEnv) return fromEnv
|
||||||
|
if (process.env.NODE_ENV === 'production') return 'https://sentry-relay.ghabilee.ir/monitoring'
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
function tracesSampleRate(): number {
|
function tracesSampleRate(): number {
|
||||||
const raw = process.env.SENTRY_TRACES_SAMPLE_RATE ?? '0.1'
|
const raw = process.env.SENTRY_TRACES_SAMPLE_RATE ?? '0.1'
|
||||||
const parsed = Number.parseFloat(raw)
|
const parsed = Number.parseFloat(raw)
|
||||||
@ -17,6 +25,7 @@ export function initServerSentry() {
|
|||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn,
|
dsn,
|
||||||
|
tunnel: sentryTunnel(),
|
||||||
environment: process.env.SENTRY_ENVIRONMENT?.trim() || process.env.NODE_ENV || 'production',
|
environment: process.env.SENTRY_ENVIRONMENT?.trim() || process.env.NODE_ENV || 'production',
|
||||||
release: process.env.SENTRY_RELEASE?.trim() || process.env.NEXT_PUBLIC_APP_VERSION || undefined,
|
release: process.env.SENTRY_RELEASE?.trim() || process.env.NEXT_PUBLIC_APP_VERSION || undefined,
|
||||||
tracesSampleRate: tracesSampleRate(),
|
tracesSampleRate: tracesSampleRate(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user