Разработана первая версия приложений

This commit is contained in:
mi
2026-07-10 18:06:14 +03:00
parent aa8761d1b3
commit 8c7b4074c4
162 changed files with 12178 additions and 16 deletions
+14
View File
@@ -0,0 +1,14 @@
FROM nginx:1.27.4-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends gettext-base curl ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /etc/nginx/generated /var/www/certbot /usr/share/nginx/html \
&& chown -R nginx:nginx /etc/nginx/generated /var/cache/nginx /var/run
COPY nginx.conf.template /etc/nginx/templates-src/nginx.conf.template
COPY templates /etc/nginx/templates-src/sites
COPY snippets /etc/nginx/snippets
COPY scripts/entrypoint.sh /usr/local/bin/han-nginx-entrypoint
RUN sed -i 's/\r$//' /usr/local/bin/han-nginx-entrypoint \
&& chmod 0555 /usr/local/bin/han-nginx-entrypoint \
&& find /etc/nginx/templates-src /etc/nginx/snippets -type f -exec chmod 0444 {} +
ENTRYPOINT ["/usr/local/bin/han-nginx-entrypoint"]
+71
View File
@@ -0,0 +1,71 @@
services:
nginx:
build:
context: .
image: han-chat-nginx:${RELEASE_VERSION:-local}
environment:
APP_ENV: ${APP_ENV:-production-like}
PUBLIC_HOST: ${PUBLIC_HOST}
NGINX_TLS_ENABLED: ${NGINX_TLS_ENABLED:-true}
NGINX_TLS_CERTIFICATE: ${NGINX_TLS_CERTIFICATE}
NGINX_TLS_CERTIFICATE_KEY: ${NGINX_TLS_CERTIFICATE_KEY}
NGINX_HSTS_MAX_AGE: ${NGINX_HSTS_MAX_AGE:-0}
NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-8m}
NGINX_RATE_LIMIT_API: ${NGINX_RATE_LIMIT_API:-60r/m}
NGINX_RATE_LIMIT_AUTH: ${NGINX_RATE_LIMIT_AUTH:-10r/m}
NGINX_RATE_LIMIT_PUBLIC: ${NGINX_RATE_LIMIT_PUBLIC:-60r/m}
NGINX_RATE_LIMIT_POLLING: ${NGINX_RATE_LIMIT_POLLING:-60r/m}
NGINX_RATE_LIMIT_DOWNLOADS: ${NGINX_RATE_LIMIT_DOWNLOADS:-30r/m}
NGINX_RATE_LIMIT_BITRIX: ${NGINX_RATE_LIMIT_BITRIX:-120r/m}
NGINX_RATE_LIMIT_WS: ${NGINX_RATE_LIMIT_WS:-30r/m}
NGINX_MESSAGE_READ_TIMEOUT_SEC: ${NGINX_MESSAGE_READ_TIMEOUT_SEC:-330}
FRONTEND_DEV_PROXY_ENABLED: ${FRONTEND_DEV_PROXY_ENABLED:-false}
EXPO_DEV_SERVER_URL: ${EXPO_DEV_SERVER_URL:-http://host.docker.internal:8081}
BITRIX_FRAME_ANCESTORS: ${BITRIX_FRAME_ANCESTORS:-https://*.bitrix24.ru}
S3_CONNECT_SRC: ${S3_CONNECT_SRC:-https://s3.storage.selcloud.ru}
ports:
- "${NGINX_HTTP_PORT:-80}:80"
- "${NGINX_HTTPS_PORT:-443}:443"
expose: ["8080"]
volumes:
- nginx-certs:/etc/letsencrypt:ro
- nginx-acme-webroot:/var/www/certbot:ro
- frontend-static:/usr/share/nginx/html:ro
- nginx-cache:/var/cache/nginx
networks: [public, backend]
depends_on:
frontend-static: {condition: service_completed_successfully}
api-backend: {condition: service_healthy}
keycloak: {condition: service_healthy}
bitrix-local-app: {condition: service_healthy}
bitrix-sync: {condition: service_healthy}
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/nginx-health/live || curl -fsS http://127.0.0.1/nginx-health/live"]
interval: 10s
timeout: 3s
retries: 10
restart: unless-stopped
read_only: true
tmpfs:
- /tmp:size=128m,mode=1777
- /etc/nginx/generated:size=4m,mode=0755,uid=101,gid=101
cap_drop: ["ALL"]
cap_add: ["NET_BIND_SERVICE", "SETUID", "SETGID"]
security_opt: ["no-new-privileges:true"]
extra_hosts:
- "host.docker.internal:host-gateway"
ulimits:
nofile: {soft: 65536, hard: 65536}
logging:
driver: json-file
options: {max-size: "50m", max-file: "5"}
certbot:
image: certbot/certbot:v3.1.0
profiles: ["certbot"]
volumes:
- nginx-certs:/etc/letsencrypt
- nginx-acme-webroot:/var/www/certbot
networks: [public]
security_opt: ["no-new-privileges:true"]
cap_drop: ["ALL"]
@@ -0,0 +1,65 @@
worker_processes auto;
pid /tmp/nginx.pid;
error_log /dev/stderr warn;
events {
worker_connections 4096;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
charset utf-8;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
map $http_x_request_id $edge_request_id {
"~*^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" $http_x_request_id;
"~*^[0-9A-HJKMNP-TV-Z]{26}$" $http_x_request_id;
default $request_id;
}
map $http_traceparent $trace_id {
"~^[0-9a-f]{2}-(?<valid_trace_id>[0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$" $valid_trace_id;
default "";
}
map $request_method $polling_key {
GET $binary_remote_addr;
default "";
}
log_format json_combined escape=json
'{"timestamp":"$time_iso8601","service.name":"nginx","request_id":"$edge_request_id",'
'"trace_id":"$trace_id","remote_addr":"$remote_addr","host":"$host","method":"$request_method",'
'"uri":"$uri","status":$status,"bytes":$body_bytes_sent,"duration":$request_time,'
'"upstream_status":"$upstream_status","upstream_time":"$upstream_response_time",'
'"cache":"$upstream_cache_status","tls":"$ssl_protocol","user_agent":"$http_user_agent"}';
access_log /dev/stdout json_combined;
limit_req_status 429;
limit_conn_status 429;
limit_req_zone $binary_remote_addr zone=api:10m rate=${NGINX_RATE_LIMIT_API};
limit_req_zone $binary_remote_addr zone=auth:10m rate=${NGINX_RATE_LIMIT_AUTH};
limit_req_zone $binary_remote_addr zone=public:10m rate=${NGINX_RATE_LIMIT_PUBLIC};
limit_req_zone $polling_key zone=polling:10m rate=${NGINX_RATE_LIMIT_POLLING};
limit_req_zone $binary_remote_addr zone=downloads:10m rate=${NGINX_RATE_LIMIT_DOWNLOADS};
limit_req_zone $binary_remote_addr zone=bitrix_callbacks:10m rate=${NGINX_RATE_LIMIT_BITRIX};
limit_req_zone $binary_remote_addr zone=ws_connect:10m rate=${NGINX_RATE_LIMIT_WS};
limit_conn_zone $binary_remote_addr zone=connections:10m;
proxy_cache_path /var/cache/nginx/public levels=1:2 keys_zone=public_cache:20m
max_size=256m inactive=1h use_temp_path=off;
client_max_body_size ${NGINX_CLIENT_MAX_BODY_SIZE};
client_body_temp_path /tmp/client_body;
proxy_temp_path /tmp/proxy;
upstream api_backend { server api-backend:8000; keepalive 32; }
upstream keycloak_upstream { server keycloak:8080; keepalive 16; }
upstream bitrix_local { server bitrix-local-app:8080; keepalive 16; }
upstream bitrix_sync_upstream { server bitrix-sync:8080; keepalive 8; }
upstream frontend_dev { server ${EXPO_DEV_SERVER_HOSTPORT}; keepalive 8; }
include /etc/nginx/generated/site.conf;
}
@@ -0,0 +1,56 @@
#!/bin/sh
set -eu
required="PUBLIC_HOST NGINX_RATE_LIMIT_API NGINX_RATE_LIMIT_AUTH NGINX_RATE_LIMIT_PUBLIC NGINX_RATE_LIMIT_POLLING NGINX_RATE_LIMIT_DOWNLOADS NGINX_RATE_LIMIT_BITRIX NGINX_RATE_LIMIT_WS NGINX_CLIENT_MAX_BODY_SIZE NGINX_MESSAGE_READ_TIMEOUT_SEC"
for name in $required; do
eval "value=\${$name:-}"
if [ -z "$value" ]; then
echo "nginx bootstrap: missing $name" >&2
exit 64
fi
done
case "${EXPO_DEV_SERVER_URL:-http://host.docker.internal:8081}" in
http://*/*) echo "nginx bootstrap: EXPO_DEV_SERVER_URL must not contain a path" >&2; exit 64 ;;
http://*) EXPO_DEV_SERVER_HOSTPORT=${EXPO_DEV_SERVER_URL#http://} ;;
*) echo "nginx bootstrap: EXPO_DEV_SERVER_URL must use http:// Docker/host DNS" >&2; exit 64 ;;
esac
export EXPO_DEV_SERVER_HOSTPORT
if [ "${FRONTEND_DEV_PROXY_ENABLED:-false}" = "true" ] \
&& { [ "${APP_ENV:-}" = "production" ] || [ "${APP_ENV:-}" = "production-like" ]; }; then
echo "nginx bootstrap: frontend dev proxy is forbidden in $APP_ENV" >&2
exit 64
fi
umask 027
common_vars='${NGINX_RATE_LIMIT_API} ${NGINX_RATE_LIMIT_AUTH} ${NGINX_RATE_LIMIT_PUBLIC} ${NGINX_RATE_LIMIT_POLLING} ${NGINX_RATE_LIMIT_DOWNLOADS} ${NGINX_RATE_LIMIT_BITRIX} ${NGINX_RATE_LIMIT_WS} ${NGINX_CLIENT_MAX_BODY_SIZE} ${EXPO_DEV_SERVER_HOSTPORT}'
site_vars='${PUBLIC_HOST} ${NGINX_TLS_CERTIFICATE} ${NGINX_TLS_CERTIFICATE_KEY} ${NGINX_MESSAGE_READ_TIMEOUT_SEC} ${BITRIX_FRAME_ANCESTORS}'
security_vars='${NGINX_HSTS_MAX_AGE} ${S3_CONNECT_SRC}'
envsubst "$common_vars" < /etc/nginx/templates-src/nginx.conf.template > /tmp/nginx.conf
envsubst "$security_vars" < /etc/nginx/templates-src/sites/security-headers.conf.template \
> /etc/nginx/generated/security-headers.conf
if [ "${FRONTEND_DEV_PROXY_ENABLED:-false}" = "true" ]; then
cp /etc/nginx/templates-src/sites/frontend-dev.conf.template /etc/nginx/generated/frontend-location.conf
else
cp /etc/nginx/templates-src/sites/frontend-static.conf.template /etc/nginx/generated/frontend-location.conf
fi
if [ "${NGINX_TLS_ENABLED:-true}" = "true" ]; then
for certificate in "${NGINX_TLS_CERTIFICATE:-}" "${NGINX_TLS_CERTIFICATE_KEY:-}"; do
if [ ! -r "$certificate" ]; then
echo "nginx bootstrap: TLS file is not readable: $certificate" >&2
exit 78
fi
done
envsubst "$site_vars" < /etc/nginx/templates-src/sites/site-tls.conf.template \
> /etc/nginx/generated/site.conf
else
envsubst '${PUBLIC_HOST}' < /etc/nginx/templates-src/sites/site-bootstrap.conf.template \
> /etc/nginx/generated/site.conf
fi
nginx -t -c /tmp/nginx.conf
exec nginx -c /tmp/nginx.conf -g 'daemon off;'
@@ -0,0 +1,16 @@
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Request-ID $edge_request_id;
proxy_set_header traceparent $http_traceparent;
proxy_hide_header X-Powered-By;
proxy_hide_header Server;
add_header X-Request-ID $edge_request_id always;
include /etc/nginx/generated/security-headers.conf;
proxy_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
@@ -0,0 +1,5 @@
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Frame-Options "DENY" always;
@@ -0,0 +1,8 @@
location / {
include /etc/nginx/snippets/proxy-common.conf;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_read_timeout 90s;
proxy_pass http://frontend_dev;
}
@@ -0,0 +1,20 @@
location ~* \.(?:css|js|png|jpe?g|gif|webp|svg|ico|woff2?)$ {
root /usr/share/nginx/html;
try_files $uri =404;
add_header Cache-Control "public, max-age=31536000, immutable";
include /etc/nginx/generated/security-headers.conf;
}
location = /index.html {
root /usr/share/nginx/html;
add_header Cache-Control "no-cache";
include /etc/nginx/generated/security-headers.conf;
}
location = /service-worker.js {
root /usr/share/nginx/html;
add_header Cache-Control "no-cache";
include /etc/nginx/generated/security-headers.conf;
}
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
@@ -0,0 +1,7 @@
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Frame-Options "DENY" always;
add_header Strict-Transport-Security "max-age=${NGINX_HSTS_MAX_AGE}" always;
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; connect-src 'self' https: wss: ${S3_CONNECT_SRC}; img-src 'self' data: blob: https:; script-src 'self'; style-src 'self' 'unsafe-inline'" always;
@@ -0,0 +1,16 @@
server {
listen 80;
server_name ${PUBLIC_HOST};
location = /nginx-health/live {
access_log off;
return 200 "ok\n";
}
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
}
location ^~ /internal/ { return 404; }
location ^~ /_internal/ { return 404; }
location / { return 308 https://$host$request_uri; }
}
@@ -0,0 +1,137 @@
server {
listen 8080;
server_name _;
location = /nginx-health/live { access_log off; return 200 "ok\n"; }
}
server {
listen 80;
server_name ${PUBLIC_HOST};
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
}
location / { return 308 https://$host$request_uri; }
}
server {
listen 443 ssl;
http2 on;
server_name ${PUBLIC_HOST};
ssl_certificate ${NGINX_TLS_CERTIFICATE};
ssl_certificate_key ${NGINX_TLS_CERTIFICATE_KEY};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5:!3DES;
ssl_session_cache shared:TLS:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
include /etc/nginx/generated/security-headers.conf;
error_page 429 = @rate_limited;
location @rate_limited {
add_header Retry-After "1" always;
add_header X-Request-ID $edge_request_id always;
include /etc/nginx/generated/security-headers.conf;
return 429;
}
location = /nginx-health/live { access_log off; return 200 "ok\n"; }
location ^~ /internal/ { return 404; }
location ^~ /_internal/ { return 404; }
location = /metrics { return 404; }
location = /nginx_status { return 404; }
location ~* /(?:\.env|docker-compose|config\.(?:json|ya?ml))$ { return 404; }
location ~ /\. { return 404; }
location = /api/v1/realtime {
limit_req zone=ws_connect burst=5 nodelay;
limit_conn connections 10;
include /etc/nginx/snippets/proxy-common.conf;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Sec-WebSocket-Protocol $http_sec_websocket_protocol;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 90s;
proxy_send_timeout 30s;
proxy_pass http://api_backend;
}
location ~ ^/api/v1/dialogs/[0-9a-fA-F-]+/messages$ {
limit_req zone=api burst=20 nodelay;
limit_req zone=polling burst=20 nodelay;
include /etc/nginx/snippets/proxy-common.conf;
proxy_read_timeout ${NGINX_MESSAGE_READ_TIMEOUT_SEC}s;
proxy_pass http://api_backend;
}
location = /api/v1/public/app-config {
limit_req zone=public burst=20 nodelay;
include /etc/nginx/snippets/proxy-common.conf;
proxy_cache public_cache;
proxy_cache_methods GET HEAD;
proxy_cache_bypass $http_authorization;
proxy_no_cache $http_authorization $upstream_http_set_cookie;
proxy_cache_valid 200 1h;
proxy_pass http://api_backend;
}
location = /api/v1/public/content {
limit_req zone=public burst=20 nodelay;
include /etc/nginx/snippets/proxy-common.conf;
proxy_cache public_cache;
proxy_cache_methods GET HEAD;
proxy_cache_bypass $http_authorization;
proxy_no_cache $http_authorization $upstream_http_set_cookie;
proxy_cache_valid 200 1h;
proxy_pass http://api_backend;
}
location ~ ^/api/v1/(?:documents|dialogs)/.+/download-url$ {
limit_req zone=downloads burst=10 nodelay;
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://api_backend;
}
location ^~ /api/ {
limit_req zone=api burst=30 nodelay;
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://api_backend;
}
location ^~ /auth/ {
limit_req zone=auth burst=10;
include /etc/nginx/snippets/proxy-common.conf;
proxy_read_timeout 60s;
proxy_pass http://keycloak_upstream;
}
location = /bitrix/handler {
limit_req zone=bitrix_callbacks burst=60 nodelay;
include /etc/nginx/snippets/proxy-common.conf;
proxy_read_timeout 60s;
proxy_buffering off;
proxy_pass http://bitrix_local;
}
location = /bitrix/install {
limit_req zone=bitrix_callbacks burst=30 nodelay;
include /etc/nginx/snippets/proxy-common.conf;
proxy_read_timeout 60s;
proxy_pass http://bitrix_local;
}
location = /bitrix/placement {
limit_req zone=bitrix_callbacks burst=30 nodelay;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Request-ID $edge_request_id;
add_header X-Request-ID $edge_request_id always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; frame-ancestors ${BITRIX_FRAME_ANCESTORS}" always;
proxy_pass http://bitrix_local;
}
location ^~ /bitrix/ { return 404; }
include /etc/nginx/generated/frontend-location.conf;
}