The deployment/ sub-project contains all Docker Compose definitions, Caddyfile configurations, Gatus monitoring config, and environment templates for running the Amanat escrow platform. Two compose files exist side-by-side reflecting a legacy setup and the current live stack.
1. Overview
File
Status
Host
Notes
deployment/docker-compose.yml
Legacy
Any
nginx + traefik_public network, images from git.manko.yoga registry
deployment/dev-amn/docker-compose.yml
Active
89.58.32.32
shared-web + infra-caddy ingress, images from git.tbs.amn.gg/escrow
The dev-amn stack is the authoritative deployment. It runs under Arcane project devEscrow (77c10db2…) on the ARM64 host at 89.58.32.32. All operational decisions, env var edits, and container restarts target this stack.
The legacy compose (deployment/docker-compose.yml) is kept for historical reference. It uses an nginx sidecar, Traefik labels, and images from the old git.manko.yoga registry. Do not deploy from it.
2. Services
Service
Image
Internal Port
Role
backend
git.tbs.amn.gg/escrow/backend:dev
5001
Express 5 API + Socket.IO + admin seed
frontend
git.tbs.amn.gg/escrow/frontend:dev
8083
Next.js SSR app
refscanner
git.tbs.amn.gg/escrow/scanner:dev
8080
In-house AMN payment scanner (SQLite)
postgres
postgres:18-alpine
5432
Primary datastore (auth, marketplace, PG stores)
redis
redis:8-alpine
6379
Session cache, rate-limit counters, job queues
mongodb
mongo:8.0-noble
27017
Legacy datastore — dev boot parity only, retired in prod
gatus
twinproduction/gatus:latest
8080 (mapped 8084)
Uptime monitoring + Telegram alerting
Note on refscanner: The in-house scanner (provider: "amn.scanner") persists state in a SQLite file at /data/scanner.db inside the container. It does not expose a port on shared-web; the backend calls it via the default bridge by container alias refscanner.
Note on mongodb: The Mongo container is retained for dev stack parity because MONGODB_URI is still present in the env. It will be removed once the backend's remaining Mongo reads are migrated to Postgres. See mongo-to-pg-migration-guide and mongo_retirement_status.
postgres, redis, mongodb are on default only — no external exposure.
refscanner is on default only; backend reaches it via alias refscanner:8080.
Any new public-facing service must join shared-web AND get a Caddyfile block. See Shared Infra (89.58.32.32) and section 6 below.
shared-web must exist on the host before docker compose up. It is created by the infra project.
5. Volumes and Bind Mounts
All data volumes in the dev-amn stack use relative bind mounts under ./data/ (resolved to /opt/arcane/data/projects/escrow-dev/data/ on the server):
Service
Host Path
Container Path
Notes
backend
./data/uploads
/app/uploads
User-uploaded files
refscanner
./data/scanner
/data
SQLite DB at /data/scanner.db
postgres
./data/postgres
/var/lib/postgresql/data
PGDATA subdir workaround: actual data at ./data/postgres/pgdata
redis
./data/redis
/data
Persistence dump
mongodb
./data/mongo
/data/db
Legacy; can be deleted once Mongo retired
gatus
./gatus/config.yaml
/config/config.yaml (ro)
Monitoring config — part of repo
Postgres volume note:postgres:18 introduced a version-scoped data directory layout and refuses to init directly into a volume root that already contains files from a different layout. The compose file sets PGDATA=/var/lib/postgresql/data/pgdata to place actual data in a subdirectory of the mount, avoiding init conflicts.
Legacy compose (deployment/docker-compose.yml) uses absolute host paths under /var/data/escrowDev/ and does not share volumes with the dev-amn stack.
6. Reverse Proxy (infra-caddy) Integration
Ingress for 89.58.32.32 is handled exclusively by infra-caddy — the Caddy container in the Arcane project infra. It owns host ports 80 and 443. No service should bind those ports directly.
Current Caddyfile block (dev.amn.gg)
Located at /opt/arcane/data/projects/infra/Caddyfile on the server (and mirrored in deployment/dev-amn/Caddyfile for reference):
The backend-dev-health check validates that all 8 domain stores are running on Postgres (auth, config, address, category, levelConfig, shopSettings, review, notification). A failure here means a store mode regression or a broken PG_URL.
Gatus dashboard is accessible at :8084 on the host (not publicly proxied by default — access via SSH tunnel or add a Caddyfile block if needed).
8. Environment Variables
All vars are passed to containers via .env at the stack root (deployment/dev-amn/.env on the server, deployment/.env in the repo as the live dev reference). The file is chmod 600 and never committed.
Woodpecker CI builds backend and frontend images, pushes tags to git.tbs.amn.gg/escrow/ on merge to dev, then triggers an Arcane GitOps sync which pulls the new image and recreates the container.
git push origin dev
└─► Woodpecker build pipeline
└─► docker push git.tbs.amn.gg/escrow/backend:dev
└─► docker push git.tbs.amn.gg/escrow/frontend:dev
└─► arcane-cli gitops sync cf6c9eab… (or watchtower polls)
└─► escrow-backend container restarted with new image
└─► escrow-frontend container restarted with new image
Always bump the version in package.json + lock before pushing, otherwise the CI build may not register as a new deploy. See version_bump_before_ci.
9.2 Manual deploy (backend hotfix — no registry)
For urgent backend fixes without a full CI cycle, use the local-build pattern (the dev stack has pull_policy: always but the override docker-compose.override.yml sets pull_policy: never for the escrow-backend-local:dev image path):
# 1. Copy changed files to build tree on server
scp -i ~/CascadeProjects/wzp src/services/auth/authRoutes.ts \
root@89.58.32.32:/tmp/escrow-backend-build/src/services/auth/
# 2. Rebuild image on server (~3 min, ARM64)
ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \
"cd /tmp/escrow-backend-build && docker build -f Dockerfile.prod -t escrow-backend-local:dev ."# 3. Restart the backend container
ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \
"cd /opt/arcane/data/projects/escrow-dev && docker compose up -d backend"
CI ✓ green does NOT guarantee the new image was pushed to the registry. Always verify curl /api/version returns the expected version. See woodpecker_silent_build_fail.
10. Dev vs Prod Differences
Aspect
dev-amn (dev.amn.gg)
Prod (amn.gg)
Compose file
deployment/dev-amn/docker-compose.yml
Separate prod stack (not in this repo)
Image registry
git.tbs.amn.gg/escrow
Same registry, prod tags
Image tag
:dev
:latest or versioned
MongoDB
Present (dev parity)
Retired
ENABLE_TESTNET_CHAINS
true (compose override)
Not set / false
NODE_ENV
production (same)
production
NEXT_PUBLIC_IS_DEVELOPMENT
false
false
PAYMENT_PROVIDER_MODE
live
live
REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS
can be true for RN testing
false
Watchtower labels
Present in legacy compose
Prod stack may differ
Gatus monitoring
Monitors both dev + prod endpoints
N/A (shared gatus instance)
TLS
Cloudflare proxy → Caddy (disable_redirects)
Same
Version bump requirement
Required before CI push
Required
11. Secret Management
The .env file on the server is the single source of runtime secrets. It is never committed.
Location on server: /opt/arcane/data/projects/escrow-dev/.env
Permissions: chmod 600 owned by root
Reference template: deployment/.env (in repo — contains live dev values, treated as low-sensitivity dev config; rotate before prod use)
.gitleaks.toml in deployment/ configures secret scanning exclusions for the repo
Rules
Never commit .env or any file containing real tokens, passwords, or private keys.
Never pass secrets as Dockerfile ARG/ENV at build time — they appear in image layers. All secrets are runtime-injected via env_file.
NEXT_PUBLIC_* vars are baked into the frontend bundle at build time. Do not place secrets in any NEXT_PUBLIC_ variable.
Wallet addresses (e.g. ESCROW_WALLET_ADDRESS) are public on-chain but still kept out of the repo for operational hygiene.
For new deployments: copy deployment/.env to the server, fill in real values, then chmod 600.
Gatus bot token and chat ID go into the same .env — they are read by the gatus container via environment: directives.
Telegram bot tokens are high-value secrets — rotate immediately if accidentally pushed.