Ужесточить контейнеры сервисов

Прикладные образы работают non-root (deal, UID 10001, HOME=/tmp); в compose добавлены read_only root FS, tmpfs /tmp, no-new-privileges, cap_drop ALL и лимиты mem/cpus. Логи stateless-сервисов вынесены в /tmp/logs. Проверено compose config (dev/prod/observability).
This commit is contained in:
Rustam Khalimov
2026-09-11 17:39:45 +03:00
parent d2b716bbf4
commit 38561528ab
9 changed files with 99 additions and 3 deletions
+2 -2
View File
@@ -27,7 +27,7 @@
| BL-KAFKA | Kafka как шина данных между сервисами (сейчас gRPC + БД-outbox) | ТЗ §12, обсуждение | P3 | DEFERRED | | BL-KAFKA | Kafka как шина данных между сервисами (сейчас gRPC + БД-outbox) | ТЗ §12, обсуждение | P3 | DEFERRED |
| BL-CF | Cloudflare / внешний периметр (сейчас Caddy, mTLS; конфиг вне кода) | ТЗ §11, техдок §10 | P2 | MANUAL | | BL-CF | Cloudflare / внешний периметр (сейчас Caddy, mTLS; конфиг вне кода) | ТЗ §11, техдок §10 | P2 | MANUAL |
| BL-CI | Внешний CI/CD + SAST + скан зависимостей в пайплайне (сейчас вручную) | техдок §8 | P2 | BACKLOG | | BL-CI | Внешний CI/CD + SAST + скан зависимостей в пайплайне (сейчас вручную) | техдок §8 | P2 | BACKLOG |
| BL-IMG-HARDEN | non-root/read-only дефолты контейнеров, seccomp/limits | техдок §10 | P2 | TECHDEBT | | BL-IMG-HARDEN | **Сделано (2026-09-11):** non-root USER (deal, UID 10001, HOME=/tmp) во всех прикладных образах; в compose — read_only root FS + tmpfs /tmp, no-new-privileges, cap_drop ALL, mem_limit/cpus; логи stateless-сервисов в /tmp/logs. Проверено `compose config` (dev/prod/observability); живой прогон — MANUAL | техдок §10 | P2 | DONE (live — MANUAL) |
| BL-BACKUP-CRON | Автоматизация бэкапов (cron/systemd-примеры есть, реальный прогон — MANUAL) | техдок §9 | P2 | MANUAL | | BL-BACKUP-CRON | Автоматизация бэкапов (cron/systemd-примеры есть, реальный прогон — MANUAL) | техдок §9 | P2 | MANUAL |
## 3. SaaS / мультитенантность ## 3. SaaS / мультитенантность
@@ -59,7 +59,7 @@
| TD-OLD-DOCS | Исторические доки несут старые термины под пометками (переписывать не нужно) | docs sweep | P3 | TECHDEBT | | TD-OLD-DOCS | Исторические доки несут старые термины под пометками (переписывать не нужно) | docs sweep | P3 | TECHDEBT |
| TD-SOURCE-PROVIDER | Провайдеры содержимого источников. **Сделано (2026-09-11):** `TelegramSourceContentProvider` + `ReadSource` RPC + `GET /api/cards/{id}/source` + UI «Обновить из источника». Осталось: провайдеры прочих источников по мере появления | generic source 2026-09-11 | P2 | TECHDEBT | | TD-SOURCE-PROVIDER | Провайдеры содержимого источников. **Сделано (2026-09-11):** `TelegramSourceContentProvider` + `ReadSource` RPC + `GET /api/cards/{id}/source` + UI «Обновить из источника». Осталось: провайдеры прочих источников по мере появления | generic source 2026-09-11 | P2 | TECHDEBT |
| TD-STORE-ATTACH | Выгрузка вложений источника в Storage-сервис адаптером. **Решение (2026-09-11): медиа-посты Telegram пропускаем** — извлечение/выгрузка не делаются. Остались на будущее: вложения прочих источников (файл/диск/таблица) и `ISourceContentProvider` для remote-просмотра | generic source 2026-09-11 | P3 | BACKLOG | | TD-STORE-ATTACH | Выгрузка вложений источника в Storage-сервис адаптером. **Решение (2026-09-11): медиа-посты Telegram пропускаем** — извлечение/выгрузка не делаются. Остались на будущее: вложения прочих источников (файл/диск/таблица) и `ISourceContentProvider` для remote-просмотра | generic source 2026-09-11 | P3 | BACKLOG |
| TD-TG-CORE-SPLIT | Перенести оставшуюся Telegram-специфику ядра (`TelegramStore`, таблицы `Dialogs`/`TgMessages`, Discovery) в telegram-сервис. **Сделано (2026-09-11):** входящий поток переведён на generic `sources.proto`/`PushSource`, `PushMessage` удалён, приём в ядре generic (`SourceIngressGrpcService`). Осталось: каталог (`SyncDialogs`/`ReportStatus`), `TelegramStore`, Discovery | generic source 2026-09-11 | P2 | TECHDEBT | | TD-TG-CORE-SPLIT | Перенос оставшейся Telegram-специфики ядра в telegram-сервис. **Закрыто (2026-09-11): не требуется.** Задача «дашборды/карточки не знают о Telegram» решена generic-контрактом источника; оставшиеся `TelegramStore`/`Dialogs`/`TgMessages`/Discovery — это состояние тенанта (ядро — владелец данных, telegram-service — stateless-шлюз), перенос отдал бы шлюзу доступ к схеме тенанта | generic source 2026-09-11 | | CLOSED |
| TD-SOURCE-CONTACTS | Квалификатор контактов знает форматы профилей (t.me/`@handle`) — вынести в расширяемые правила источников | generic source 2026-09-11 | P3 | BACKLOG | | TD-SOURCE-CONTACTS | Квалификатор контактов знает форматы профилей (t.me/`@handle`) — вынести в расширяемые правила источников | generic source 2026-09-11 | P3 | BACKLOG |
| TD-APIMAP-COUNT | Ручной подсчёт числа операторских ручек в `api-map` (может расходиться с группировкой) | docs sweep | P3 | TECHDEBT | | TD-APIMAP-COUNT | Ручной подсчёт числа операторских ручек в `api-map` (может расходиться с группировкой) | docs sweep | P3 | TECHDEBT |
+30
View File
@@ -26,6 +26,17 @@
# dotnet run, и сервисы ходят в ингресс хоста через DEAL_CORE_INGRESS=http://host.docker.internal:5082 # dotnet run, и сервисы ходят в ингресс хоста через DEAL_CORE_INGRESS=http://host.docker.internal:5082
# (см. env telegram-service ниже). # (см. env telegram-service ниже).
# Общий hardening прикладных сервисов (BL-IMG-HARDEN): read-only root FS + tmpfs /tmp,
# запрет новых привилегий и сброс capabilities. Данные — только в именованных volume.
x-service-hardening: &service-hardening
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
services: services:
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
@@ -72,6 +83,9 @@ services:
context: .. context: ..
dockerfile: src/core/Deal.Api/Dockerfile dockerfile: src/core/Deal.Api/Dockerfile
container_name: deal-core container_name: deal-core
<<: *service-hardening
mem_limit: 1g
cpus: 1.5
environment: environment:
ASPNETCORE_ENVIRONMENT: Development ASPNETCORE_ENVIRONMENT: Development
ASPNETCORE_URLS: http://0.0.0.0:5080 ASPNETCORE_URLS: http://0.0.0.0:5080
@@ -123,11 +137,15 @@ services:
context: .. context: ..
dockerfile: src/telegram-service/Deal.Telegram/Dockerfile dockerfile: src/telegram-service/Deal.Telegram/Dockerfile
container_name: deal-telegram-service container_name: deal-telegram-service
<<: *service-hardening
mem_limit: 512m
cpus: 1.0
environment: environment:
GRPC_PORT: "5101" GRPC_PORT: "5101"
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token} DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
DEAL_TELEGRAM_SESSION_KEY: ${DEAL_TELEGRAM_SESSION_KEY:-ZmVkY2JhOTg3NjU0MzIxMGZlZGNiYTk4NzY1NDMyMTA=} DEAL_TELEGRAM_SESSION_KEY: ${DEAL_TELEGRAM_SESSION_KEY:-ZmVkY2JhOTg3NjU0MzIxMGZlZGNiYTk4NzY1NDMyMTA=}
DEAL_TELEGRAM_SESSION_DIR: /data/sessions DEAL_TELEGRAM_SESSION_DIR: /data/sessions
DEAL_LOGS_DIR: /tmp/logs
SERVICES__CORE__INGRESS: ${DEAL_CORE_INGRESS:-http://core:5082} SERVICES__CORE__INGRESS: ${DEAL_CORE_INGRESS:-http://core:5082}
ports: ports:
- "5101:5101" - "5101:5101"
@@ -151,9 +169,13 @@ services:
context: .. context: ..
dockerfile: src/ai-service/Deal.Ai/Dockerfile dockerfile: src/ai-service/Deal.Ai/Dockerfile
container_name: deal-ai-service container_name: deal-ai-service
<<: *service-hardening
mem_limit: 512m
cpus: 1.0
environment: environment:
GRPC_PORT: "5102" GRPC_PORT: "5102"
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token} DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
DEAL_LOGS_DIR: /tmp/logs
ports: ports:
- "5102:5102" - "5102:5102"
healthcheck: healthcheck:
@@ -173,10 +195,14 @@ services:
context: .. context: ..
dockerfile: src/ml-service/Deal.Ml/Dockerfile dockerfile: src/ml-service/Deal.Ml/Dockerfile
container_name: deal-ml-service container_name: deal-ml-service
<<: *service-hardening
mem_limit: 768m
cpus: 1.0
environment: environment:
GRPC_PORT: "5103" GRPC_PORT: "5103"
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token} DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
DEAL_ML_DATA_DIR: /data/ml # файлы моделей data/ml/<tenantId>.sqlite на volume deal_ml_data (Ruling 4/12) DEAL_ML_DATA_DIR: /data/ml # файлы моделей data/ml/<tenantId>.sqlite на volume deal_ml_data (Ruling 4/12)
DEAL_LOGS_DIR: /tmp/logs
ports: ports:
- "5103:5103" - "5103:5103"
volumes: volumes:
@@ -195,6 +221,9 @@ services:
context: .. context: ..
dockerfile: src/storage-service/Deal.Storage/Dockerfile dockerfile: src/storage-service/Deal.Storage/Dockerfile
container_name: deal-storage-service container_name: deal-storage-service
<<: *service-hardening
mem_limit: 512m
cpus: 1.0
environment: environment:
GRPC_PORT: "5104" GRPC_PORT: "5104"
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token} DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
@@ -203,6 +232,7 @@ services:
DEAL_STORAGE_SECRET_KEY: deal_minio_secret DEAL_STORAGE_SECRET_KEY: deal_minio_secret
DEAL_STORAGE_BUCKET: deal-attachments DEAL_STORAGE_BUCKET: deal-attachments
DEAL_STORAGE_SECURE: "false" DEAL_STORAGE_SECURE: "false"
DEAL_LOGS_DIR: /tmp/logs
ports: ports:
- "5104:5104" - "5104:5104"
depends_on: depends_on:
+26
View File
@@ -48,6 +48,17 @@
# Frontend: статика монтируется из ../src/frontend/dist (СОБРАТЬ ДО ПОДЪЁМА: cd src/frontend && # Frontend: статика монтируется из ../src/frontend/dist (СОБРАТЬ ДО ПОДЪЁМА: cd src/frontend &&
# npm run build). Конфигурация caddy — deploy/caddy/Caddyfile (см. его шапку про домен/TLS). # npm run build). Конфигурация caddy — deploy/caddy/Caddyfile (см. его шапку про домен/TLS).
# Общий hardening прикладных сервисов (BL-IMG-HARDEN): read-only root FS + tmpfs /tmp,
# запрет новых привилегий и сброс capabilities. Данные/логи — только в именованных volume или tmpfs.
x-service-hardening: &service-hardening
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
services: services:
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
@@ -84,6 +95,9 @@ services:
build: build:
context: .. context: ..
dockerfile: src/core/Deal.Api/Dockerfile dockerfile: src/core/Deal.Api/Dockerfile
<<: *service-hardening
mem_limit: 1g
cpus: 1.5
environment: environment:
ASPNETCORE_ENVIRONMENT: Production ASPNETCORE_ENVIRONMENT: Production
ASPNETCORE_URLS: http://0.0.0.0:5080 ASPNETCORE_URLS: http://0.0.0.0:5080
@@ -149,11 +163,15 @@ services:
build: build:
context: .. context: ..
dockerfile: src/telegram-service/Deal.Telegram/Dockerfile dockerfile: src/telegram-service/Deal.Telegram/Dockerfile
<<: *service-hardening
mem_limit: 512m
cpus: 1.0
environment: environment:
GRPC_PORT: "5101" GRPC_PORT: "5101"
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан} DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
DEAL_TELEGRAM_SESSION_KEY: ${DEAL_TELEGRAM_SESSION_KEY:?DEAL_TELEGRAM_SESSION_KEY не задан (ключ AES-GCM сессий)} DEAL_TELEGRAM_SESSION_KEY: ${DEAL_TELEGRAM_SESSION_KEY:?DEAL_TELEGRAM_SESSION_KEY не задан (ключ AES-GCM сессий)}
DEAL_TELEGRAM_SESSION_DIR: /data/sessions DEAL_TELEGRAM_SESSION_DIR: /data/sessions
DEAL_LOGS_DIR: /tmp/logs
SERVICES__CORE__INGRESS: ${DEAL_CORE_INGRESS:-http://core:5082} SERVICES__CORE__INGRESS: ${DEAL_CORE_INGRESS:-http://core:5082}
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0} DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
@@ -176,9 +194,13 @@ services:
build: build:
context: .. context: ..
dockerfile: src/ai-service/Deal.Ai/Dockerfile dockerfile: src/ai-service/Deal.Ai/Dockerfile
<<: *service-hardening
mem_limit: 512m
cpus: 1.0
environment: environment:
GRPC_PORT: "5102" GRPC_PORT: "5102"
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан} DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
DEAL_LOGS_DIR: /tmp/logs
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0} DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/ai-service-server.pfx DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/ai-service-server.pfx
@@ -199,10 +221,14 @@ services:
build: build:
context: .. context: ..
dockerfile: src/ml-service/Deal.Ml/Dockerfile dockerfile: src/ml-service/Deal.Ml/Dockerfile
<<: *service-hardening
mem_limit: 768m
cpus: 1.0
environment: environment:
GRPC_PORT: "5103" GRPC_PORT: "5103"
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан} DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
DEAL_ML_DATA_DIR: /data/ml DEAL_ML_DATA_DIR: /data/ml
DEAL_LOGS_DIR: /tmp/logs
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0} DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/ml-service-server.pfx DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/ml-service-server.pfx
@@ -428,8 +428,15 @@ DEAL_MTLS_ENABLED=0|1 DEAL_MTLS_CERT_PASSWORD=... DEAL_DEFAULT_AI_BU
- Криптография/код: пароли Argon2id; секреты настроек AES-256-GCM (`enc:`, ключ - Криптография/код: пароли Argon2id; секреты настроек AES-256-GCM (`enc:`, ключ
`DEAL_ENCRYPTION_KEY`); сессии Telegram AES-256-GCM (`DEAL_TELEGRAM_SESSION_KEY`); SQL `DEAL_ENCRYPTION_KEY`); сессии Telegram AES-256-GCM (`DEAL_TELEGRAM_SESSION_KEY`); SQL
параметризуется; секреты в логи/аудит не пишутся. параметризуется; секреты в логи/аудит не пишутся.
- **Hardening контейнеров (BL-IMG-HARDEN, 2026-09-11):** прикладные образы (core/telegram/ai/ml/storage)
работают non-root (пользователь `deal`, UID 10001) с `HOME=/tmp`; в compose заданы `read_only: true`,
`tmpfs: /tmp`, `security_opt: no-new-privileges`, `cap_drop: ALL` и лимиты `mem_limit`/`cpus` (якорь
`x-service-hardening`). Данные — в именованных volume (`/app/data` core, `/data/sessions` telegram,
`/data/ml` ml); логи stateless-сервисов — `DEAL_LOGS_DIR=/tmp/logs` (tmpfs), у core — volume
`/app/data/logs`. Проверено `docker compose config` (dev и prod, включая профиль observability);
живой подъём с этими ограничениями — ⚠ Manual.
- **Вне этапа (не настроено; заделы §11/roadmap):** Cloudflare (конфигурация вне кода — шапка - **Вне этапа (не настроено; заделы §11/roadmap):** Cloudflare (конфигурация вне кода — шапка
Caddyfile), k8s, non-root/read-only-дефолты образов, биллинг, UI админок, саморегистрация. Caddyfile), k8s, биллинг, UI админок, саморегистрация.
--- ---
+5
View File
@@ -35,4 +35,9 @@ COPY --from=ghcr.io/grpc-ecosystem/grpc-health-probe:v0.4.35 /ko-app/grpc-health
# Volume-ов нет: ai-service без БД и без персистентных файлов (Ruling 5) — конфиг провайдера # Volume-ов нет: ai-service без БД и без персистентных файлов (Ruling 5) — конфиг провайдера
# (id/base/model/apiKey/api_style) приходит в теле каждого запроса от ядра, ответы не хранятся. # (id/base/model/apiKey/api_style) приходит в теле каждого запроса от ядра, ответы не хранятся.
# Non-root: процесс работает пользователем deal (UID 10001); персистентных файлов нет.
RUN useradd --uid 10001 --shell /usr/sbin/nologin --no-create-home deal
ENV HOME=/tmp
USER deal
ENTRYPOINT ["dotnet", "Deal.Ai.dll"] ENTRYPOINT ["dotnet", "Deal.Ai.dll"]
+8
View File
@@ -32,4 +32,12 @@ COPY --from=ghcr.io/grpc-ecosystem/grpc-health-probe:v0.4.35 /ko-app/grpc-health
# (/app/data); каталог монтируется volume-ом deal_api_data из compose.dev.yml. В полном dev-стеке файлы # (/app/data); каталог монтируется volume-ом deal_api_data из compose.dev.yml. В полном dev-стеке файлы
# идут в MinIO (Storage__Minio__* env, см. compose.dev.yml) — volume остаётся для ключа шифрования. # идут в MinIO (Storage__Minio__* env, см. compose.dev.yml) — volume остаётся для ключа шифрования.
# Non-root: процесс работает пользователем deal (UID 10001); /app/data — каталог данных (volume).
# HOME=/tmp — рабочий каталог для временных/DP-файлов (совместимо с read_only root FS + tmpfs /tmp).
RUN useradd --uid 10001 --shell /usr/sbin/nologin --no-create-home deal \
&& mkdir -p /app/data \
&& chown -R deal:deal /app/data
ENV HOME=/tmp
USER deal
ENTRYPOINT ["dotnet", "Deal.Api.dll"] ENTRYPOINT ["dotnet", "Deal.Api.dll"]
+7
View File
@@ -35,4 +35,11 @@ COPY --from=ghcr.io/grpc-ecosystem/grpc-health-probe:v0.4.35 /ko-app/grpc-health
# Файлы моделей тенантов — data/ml/<tenantId>.sqlite (Ruling 4): каталог монтируется volume-ом # Файлы моделей тенантов — data/ml/<tenantId>.sqlite (Ruling 4): каталог монтируется volume-ом
# deal_ml_data из compose.dev.yml; каталог и файлы создаёт движок модели (задачи 5–6). # deal_ml_data из compose.dev.yml; каталог и файлы создаёт движок модели (задачи 5–6).
# Non-root: процесс работает пользователем deal (UID 10001); /data/ml — каталог моделей (volume).
RUN useradd --uid 10001 --shell /usr/sbin/nologin --no-create-home deal \
&& mkdir -p /data/ml \
&& chown -R deal:deal /data/ml
ENV HOME=/tmp
USER deal
ENTRYPOINT ["dotnet", "Deal.Ml.dll"] ENTRYPOINT ["dotnet", "Deal.Ml.dll"]
@@ -23,4 +23,10 @@ WORKDIR /app
EXPOSE 5104 EXPOSE 5104
COPY --from=build /app/publish . COPY --from=build /app/publish .
COPY --from=ghcr.io/grpc-ecosystem/grpc-health-probe:v0.4.35 /ko-app/grpc-health-probe /bin/grpc_health_probe COPY --from=ghcr.io/grpc-ecosystem/grpc-health-probe:v0.4.35 /ko-app/grpc-health-probe /bin/grpc_health_probe
# Non-root: процесс работает пользователем deal (UID 10001); персистентных файлов нет.
RUN useradd --uid 10001 --shell /usr/sbin/nologin --no-create-home deal
ENV HOME=/tmp
USER deal
ENTRYPOINT ["dotnet", "Deal.Storage.dll"] ENTRYPOINT ["dotnet", "Deal.Storage.dll"]
@@ -35,4 +35,11 @@ COPY --from=ghcr.io/grpc-ecosystem/grpc-health-probe:v0.4.35 /ko-app/grpc-health
# Сессии тенантов — файлы /data/sessions (Ruling 3): каталог монтируется volume-ом deal_tg_sessions # Сессии тенантов — файлы /data/sessions (Ruling 3): каталог монтируется volume-ом deal_tg_sessions
# из compose.dev.yml; создаётся при первом сохранении сессии (задачи 9–11). # из compose.dev.yml; создаётся при первом сохранении сессии (задачи 9–11).
# Non-root: процесс работает пользователем deal (UID 10001); /data/sessions — каталог сессий (volume).
RUN useradd --uid 10001 --shell /usr/sbin/nologin --no-create-home deal \
&& mkdir -p /data/sessions \
&& chown -R deal:deal /data/sessions
ENV HOME=/tmp
USER deal
ENTRYPOINT ["dotnet", "Deal.Telegram.dll"] ENTRYPOINT ["dotnet", "Deal.Telegram.dll"]