init222228
This commit is contained in:
34
.env
34
.env
@@ -1,34 +0,0 @@
|
|||||||
# Local .env for docker compose ${REDIS_PASSWORD} interpolation.
|
|
||||||
# DO NOT COMMIT (already in .gitignore). На прод-боксе оператор создаёт свой через `cp .env.example .env`.
|
|
||||||
|
|
||||||
VAULT_ADDR=
|
|
||||||
VAULT_ROLE_ID=
|
|
||||||
VAULT_SECRET_ID=
|
|
||||||
VAULT_MOUNT_POINT=dev-secrets
|
|
||||||
VAULT_SECRET_PATH=database
|
|
||||||
VAULT_JWT_KID_PATH=jwt/kid
|
|
||||||
VAULT_JWT_KIDS_PREFIX=jwt/kids
|
|
||||||
VAULT_CSRF_PATH=csrf
|
|
||||||
VAULT_CRYPTO_KEY_PATH=crypto/master
|
|
||||||
|
|
||||||
JWT_ALGORITHM=RS256
|
|
||||||
JWT_ISSUER=bitok
|
|
||||||
JWT_AUDIENCE=elcsa
|
|
||||||
|
|
||||||
API_PORT=3001
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
|
|
||||||
CORS_ORIGINS=*
|
|
||||||
CORS_ALLOW_CREDENTIALS=false
|
|
||||||
|
|
||||||
REDIS_HOST=keydb
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=0O7klMYUvwwR19UORSzEtsRn9kUPnDyfkJ9GDH2yMERYV0vRCU
|
|
||||||
REDIS_DB=0
|
|
||||||
|
|
||||||
# Price oracle (CoinGecko free tier — без ключа работает).
|
|
||||||
COINGECKO_API_KEY=
|
|
||||||
|
|
||||||
# Outbound proxy для swap + bridge endpoints.
|
|
||||||
# Если задан — Jupiter/Relay/RPC calls идут через proxy. Read-only direct.
|
|
||||||
OUTBOUND_PROXY_URL=http://37.220.84.34:3128
|
|
||||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
node_modules/
|
||||||
|
**/node_modules/
|
||||||
|
dist/
|
||||||
|
**/dist/
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
38
README.md
38
README.md
@@ -28,55 +28,29 @@ vault kv put dev-secrets/jwt/kids/<kid-from-bitok> \
|
|||||||
|
|
||||||
⚠️ **Master-key менять нельзя** — все existing `encrypted_mnemonic` станут нерасшифровываемыми. API на старте делает self-test: пытается декриптить любой существующий mnemonic и фейлится если ключ не подошёл.
|
⚠️ **Master-key менять нельзя** — все existing `encrypted_mnemonic` станут нерасшифровываемыми. API на старте делает self-test: пытается декриптить любой существующий mnemonic и фейлится если ключ не подошёл.
|
||||||
|
|
||||||
## Deploy (через docker compose)
|
## Deploy
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Залить bundle на сервер
|
# Залить bundle на сервер
|
||||||
scp -P 2222 -r deployserver/ server@<host>:~/cryptowallet/
|
scp -P 2222 -r deployserver/ server@<host>:~/cryptowallet/
|
||||||
|
|
||||||
# 2. На сервере: заполнить .env
|
# На сервере: убедись что .env заполнен (VAULT_*, JWT_*, CORS_ORIGINS, REDIS_PASSWORD, ...)
|
||||||
ssh server@<host> -p 2222
|
ssh server@<host> -p 2222
|
||||||
cd ~/cryptowallet/deployserver
|
cd ~/cryptowallet/deployserver
|
||||||
cp .env.example .env
|
|
||||||
chmod 600 .env
|
|
||||||
nano .env # заполни VAULT_*, JWT_*, REDIS_PASSWORD, POSTGRES_PASSWORD, CORS_ORIGINS
|
|
||||||
|
|
||||||
# 3. Поднять stack (api + postgres + keydb)
|
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
|
|
||||||
# 4. Проверить
|
|
||||||
docker compose ps
|
|
||||||
docker compose logs -f api
|
docker compose logs -f api
|
||||||
curl http://localhost:3001/api/health
|
curl http://localhost:3001/api/health
|
||||||
```
|
```
|
||||||
|
|
||||||
В `.env` **обязательны**: `VAULT_ADDR`, `VAULT_ROLE_ID`, `VAULT_SECRET_ID`, `JWT_ISSUER=bitok`, `JWT_AUDIENCE`, `CORS_ORIGINS`, `POSTGRES_PASSWORD`, `REDIS_PASSWORD`.
|
API **не делает migrations / DROP / ALTER** при старте — только INSERT/UPDATE/SELECT. Schema (если нужны новые колонки/таблицы для нового функционала) обновляется только руками: `psql -f cryptowallet-schema.sql` (script append-only — `CREATE TABLE IF NOT EXISTS` / `ALTER TABLE ADD COLUMN IF NOT EXISTS`, никаких DROP).
|
||||||
|
|
||||||
**Что в compose:**
|
|
||||||
- `api` — наш Node API из multi-stage Dockerfile (read-only fs, uid 1001, port 127.0.0.1:3001)
|
|
||||||
- `postgres` — хранилище (internal only, без exposed ports)
|
|
||||||
- `keydb` — Redis-compatible для idempotency + asset map cache (internal only)
|
|
||||||
|
|
||||||
**Чего НЕТ в compose** (production не использует — оператор настраивает отдельно):
|
|
||||||
- HashiCorp Vault — production использует HA cluster (URL в `VAULT_ADDR`)
|
|
||||||
- JWT issuer — production принимает JWT от bitok external service
|
|
||||||
- Web UI — frontend деплоится отдельно (Vercel / nginx / CDN)
|
|
||||||
- Nginx reverse proxy + TLS — оператор сам ставит перед `127.0.0.1:3001`
|
|
||||||
|
|
||||||
**Если используешь managed Postgres** (e.g. AWS RDS):
|
|
||||||
- Раскомментируй `DATABASE_URL` в `.env`
|
|
||||||
- Закомментируй `postgres` service в `docker-compose.yml` + убери `depends_on.postgres` из `api`
|
|
||||||
|
|
||||||
## Update / Rebuild
|
## Update / Rebuild
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Только app (postgres/keydb остаются — данные не теряются)
|
# Залить новый src + rebuild api (keydb данные не теряются)
|
||||||
scp -P 2222 -r deployserver/apps deployserver/Dockerfile deployserver/docker-compose.yml \
|
scp -P 2222 -r deployserver/apps deployserver/Dockerfile deployserver/docker-compose.yml \
|
||||||
server@<host>:~/cryptowallet/deployserver/
|
server@<host>:~/cryptowallet/deployserver/
|
||||||
ssh server@<host> -p 2222 'cd cryptowallet/deployserver && docker compose up -d --build api'
|
ssh server@<host> -p 2222 'cd cryptowallet/deployserver && docker compose up -d --build'
|
||||||
|
|
||||||
# Полный rebuild
|
|
||||||
ssh server@<host> -p 2222 'cd cryptowallet/deployserver && docker compose up -d --build --force-recreate'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|||||||
@@ -1,123 +1,16 @@
|
|||||||
# ─────────────────────────────────────────────────────────────────────
|
|
||||||
# Production docker-compose для CryptoWallet API.
|
|
||||||
#
|
|
||||||
# Что в этом stack:
|
|
||||||
# - api — наш Node.js API (multi-stage build из ./Dockerfile)
|
|
||||||
# - postgres — хранилище encrypted_mnemonic + audit_log
|
|
||||||
# - keydb — Redis-compatible для idempotency cache + asset map cache
|
|
||||||
#
|
|
||||||
# Что НЕ включено (production не использует — оператор настраивает отдельно):
|
|
||||||
# - Vault — production использует HashiCorp Vault HA cluster (VAULT_ADDR в .env)
|
|
||||||
# - JWT signer— production принимает JWT от bitok (внешний сервис, JWT_ISSUER в .env)
|
|
||||||
# - web-ui — фронтенд деплоится отдельно (Vercel / nginx CDN / etc.)
|
|
||||||
# - vault-init — production Vault уже инициализирован оператором (см. README pre-deploy)
|
|
||||||
#
|
|
||||||
# Security hardening:
|
|
||||||
# - api запускается под uid 1001, read-only fs, cap_drop ALL, no-new-privileges
|
|
||||||
# - api порт 3001 биндится на 127.0.0.1 (наружу через nginx + TLS, оператор)
|
|
||||||
# - postgres + keydb без exposed ports (только internal docker network)
|
|
||||||
# - .env с secrets — 600 permission, не в репо
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# cp .env.example .env # заполнить все VAULT_*, JWT_*, REDIS_PASSWORD, CORS_ORIGINS, etc.
|
|
||||||
# chmod 600 .env
|
|
||||||
# docker compose up -d --build
|
|
||||||
# docker compose logs -f api
|
|
||||||
# ─────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
api:
|
|
||||||
build:
|
|
||||||
# Context = parent dir (мы внутри deployserver/, апи берёт apps/api/ из родителя)
|
|
||||||
context: ..
|
|
||||||
dockerfile: deployserver/Dockerfile
|
|
||||||
image: cryptowallet-api:latest
|
|
||||||
container_name: cw-api
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
keydb:
|
|
||||||
condition: service_healthy
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
# Override DB/Redis host для docker network (если в .env стоят prod hosts —
|
|
||||||
# внутри compose их replace на internal service names)
|
|
||||||
POSTGRES_HOST: postgres
|
|
||||||
POSTGRES_PORT: "5432"
|
|
||||||
REDIS_HOST: keydb
|
|
||||||
REDIS_PORT: "6379"
|
|
||||||
ports:
|
|
||||||
# Loopback only — наружу через nginx reverse proxy + TLS
|
|
||||||
- "127.0.0.1:3001:3001"
|
|
||||||
read_only: true
|
|
||||||
tmpfs:
|
|
||||||
- /tmp
|
|
||||||
cap_drop:
|
|
||||||
- ALL
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 512M
|
|
||||||
cpus: "1.0"
|
|
||||||
pids: 200
|
|
||||||
reservations:
|
|
||||||
memory: 256M
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "-qO-", "http://localhost:3001/api/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 15s
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "5"
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: cw-postgres
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER required}
|
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD required}
|
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-cryptowallet}
|
|
||||||
volumes:
|
|
||||||
- pgdata:/var/lib/postgresql/data
|
|
||||||
# Bind-mount schema.sql если оператор хочет авто-init на свежей БД.
|
|
||||||
# На existing — pg ignores 01-schema.sql (initdb запускается только если /var/lib/postgresql/data пуст).
|
|
||||||
# См. README — лучше прогонять schema руками: psql -f cryptowallet-schema.sql
|
|
||||||
# - ./cryptowallet-schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
|
|
||||||
# Internal only — никаких ports на host
|
|
||||||
expose:
|
|
||||||
- "5432"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB:-cryptowallet}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 5
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "5"
|
|
||||||
|
|
||||||
keydb:
|
keydb:
|
||||||
image: eqalpha/keydb:alpine
|
image: eqalpha/keydb
|
||||||
container_name: cw-keydb
|
container_name: cryptowallet-keydb
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
|
||||||
- keydb_data:/data
|
|
||||||
expose:
|
expose:
|
||||||
- "6379"
|
- "6379"
|
||||||
|
volumes:
|
||||||
|
- keydb_data:/data
|
||||||
command:
|
command:
|
||||||
- keydb-server
|
- keydb-server
|
||||||
- --requirepass
|
- --requirepass
|
||||||
- "${REDIS_PASSWORD:?REDIS_PASSWORD required}"
|
- ${REDIS_PASSWORD}
|
||||||
- --dir
|
- --dir
|
||||||
- /data
|
- /data
|
||||||
- --appendonly
|
- --appendonly
|
||||||
@@ -133,29 +26,50 @@ services:
|
|||||||
- --save
|
- --save
|
||||||
- "60"
|
- "60"
|
||||||
- "10000"
|
- "10000"
|
||||||
- --maxmemory
|
|
||||||
- "256mb"
|
|
||||||
- --maxmemory-policy
|
|
||||||
- "allkeys-lru"
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
|
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 2s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: cryptowallet-api
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
keydb:
|
||||||
|
condition: service_healthy
|
||||||
|
# Production: port открыт на all interfaces. TLS/WAF обязательно на reverse proxy.
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
API_PORT: "3001"
|
||||||
|
# Container hardening — post-RCE blast radius minimization.
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
pids_limit: 256
|
||||||
|
mem_limit: 512m
|
||||||
|
cpus: "1.0"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "--tries=1", "--timeout=3", "http://localhost:3001/api/health"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 3s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
deploy:
|
start_period: 15s
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 320M
|
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options:
|
options:
|
||||||
max-size: "10m"
|
max-size: "20m"
|
||||||
max-file: "5"
|
max-file: "5"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
|
||||||
keydb_data:
|
keydb_data:
|
||||||
|
|
||||||
networks:
|
|
||||||
default:
|
|
||||||
driver: bridge
|
|
||||||
|
|||||||
Reference in New Issue
Block a user