# Deploy Meshkee CMS API (Debian VM) Stack: Docker (Postgres + Redis) → Node build on server → PM2 → Nginx + Let's Encrypt. App path on server: `/opt/meshkee/app` API domain: `api.meshkee.com` → `https://api.meshkee.com/api/v1` Per-business API aliases (same Nest app on this VM): `api.{apex}` e.g. `api.sanihome.ir` → `https://api.sanihome.ir/api/v1`. Storefronts still pass the **website apex** in paths (`/tenants/sanihome.ir/...`); only the API hostname changes. > **Deploy path:** push to `git.meshkee.com` (`Meshkee/backend`), then on the API VM `git fetch` + `reset --hard origin/main` + build + `pm2 restart`. Do **not** rsync the app as the normal update path. The API VM uses a read-only SSH deploy key (`meshkee-api-vm-deploy`). Pin `sharp@0.33.5` — this VM CPU lacks x64-v2 required by sharp 0.35+. ## Prerequisites - Debian VM with SSH access - Domain `A` record pointing at the VM (for HTTPS) - Git remote with this codebase (private repo → deploy key) - Production secrets (JWT, Postgres password, S3 keys) ## 1. Server packages ```bash sudo apt update && sudo apt upgrade -y sudo apt install -y ca-certificates curl gnupg git nginx ufw # Docker sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo usermod -aG docker "$USER" # log out/in (or newgrp docker) so docker works without sudo # Node.js 20 LTS curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs sudo npm install -g pm2 # Certbot (after Nginx is installed) sudo apt install -y certbot python3-certbot-nginx ``` Firewall: ```bash sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' sudo ufw --force enable ``` ## 2. Clone the app ```bash sudo mkdir -p /opt/meshkee sudo chown "$USER:$USER" /opt/meshkee cd /opt/meshkee git clone app cd app ``` Private repo: create an SSH deploy key on the VM (`ssh-keygen -t ed25519 -C "meshkee-deploy"`), add the public key as a read-only deploy key on GitHub/GitLab, clone via SSH URL. ## 3. Production env ```bash cp .env.example .env nano .env # set strong secrets — never commit this file ``` Required production values: - Strong `POSTGRES_PASSWORD` and matching `DATABASE_URL` - Long random `JWT_ACCESS_SECRET` / `JWT_REFRESH_SECRET` - Real `S3_ACCESS_KEY_ID` / `S3_SECRET_ACCESS_KEY` - `PORT=3000` - `SMS_ENABLED` as needed ## 4. Database + Redis ```bash cd /opt/meshkee/app docker compose up -d docker compose ps ``` First Postgres volume init runs SQL under `database/migrations/` automatically. Later schema updates: ```bash ./database/migrate.sh ``` ## 5. Build and run (on the server) ```bash cd /opt/meshkee/app npm ci npm run prisma:generate npm run build ``` Production seed (super admin only — skip sample data): ```bash ./database/seed.sh database/seeds/002_super_admin_user.sql # optional reference data: # ./database/seed.sh database/seeds/004_iran_cities.sql # ./database/seed.sh database/seeds/005_business_categories.sql ``` Start with PM2: ```bash pm2 start ecosystem.config.js pm2 save pm2 startup # run the command it prints (usually with sudo) ``` Health check locally on the VM: ```bash curl -s http://127.0.0.1:3000/api/v1/ | head # or hit a known public route such as tenant resolve ``` ## 6. Nginx + HTTPS ### DNS (per business website domain) On the **business domain** DNS (e.g. zone `sanihome.ir`), add a subdomain that points at this **API VM** (same target as `api.meshkee.com`): | Type | Name / host | Value | Notes | |------|-------------|-------|--------| | **A** (preferred) | `api` | `` | Resolves `api.sanihome.ir` → API server | | **CNAME** (alternative) | `api` | `api.meshkee.com` | Same effect if your DNS panel allows CNAME on subdomains | Do **not** point `api.{apex}` at the websites VM or dashboards VM — only the Nest API VM. Repeat for each storefront apex (`api.ali-mohammadi.ir`, etc.). Central Meshkee DNS already has `api.meshkee.com` → this VM. ### Nginx Create `/etc/nginx/sites-available/meshkee-api`: ```nginx server { listen 80; # Central + per-business aliases (add more api.{apex} as domains go live) server_name api.meshkee.com api.sanihome.ir; client_max_body_size 15M; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; 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; } } ``` Enable and get certificates: ```bash sudo ln -sf /etc/nginx/sites-available/meshkee-api /etc/nginx/sites-enabled/ sudo rm -f /etc/nginx/sites-enabled/default sudo nginx -t && sudo systemctl reload nginx sudo certbot --nginx -d api.meshkee.com -d api.sanihome.ir # later, when another business goes live: # sudo certbot --nginx -d api.meshkee.com -d api.sanihome.ir -d api.other-site.ir ``` API base URLs (identical Nest routes): - Central: `https://api.meshkee.com/api/v1` - Alias example: `https://api.sanihome.ir/api/v1` ### Website API docs (global link for storefront teams) After deploy, these are public (no auth): - Hub: `https://api.meshkee.com/docs/website` - OpenAPI: `https://api.meshkee.com/docs/website/openapi.json` - Postman: `https://api.meshkee.com/docs/website/Meshkee-Website-API.postman_collection.json` - AI brief: `https://api.meshkee.com/docs/website/AI_PROMPT.md` Files live in `docs/website-api/` and are served by Nest from process cwd. Keep that folder on the VM when deploying. ### Automated cert host list (API VPS) After deploy, a sync agent on this VM can pull names to cover: ```bash curl -s -H "X-SSL-Sync-Token: $SSL_SYNC_TOKEN" \ https://api.meshkee.com/api/v1/internal/ssl/api-hosts # → { "hosts": ["api.ali-mohammadi.ir", "api.meshkee.com", "api.sanihome.ir", ...] } ``` Dashboards VPS keeps using `GET /api/v1/internal/ssl/hosts` (`business.` / `customer.` / `manage`) — do not mix the two lists. ## Ongoing updates (git only) From your laptop: 1. Commit and **push** to `origin/main` (`git.meshkee.com/Meshkee/backend`). 2. Deploy on the API VM from that commit (never rsync the tree as the primary path): ```bash ssh -i ~/.ssh/id_ed25519 root@185.164.72.119 'bash -s' <<'REMOTE' set -euo pipefail cd /opt/meshkee/app git fetch origin git reset --hard origin/main ./database/migrate.sh npm ci npm run prisma:generate npm run build pm2 restart meshkee-api REMOTE ``` Preserve `/opt/meshkee/app/.env` on the server. App remote must be SSH: `git@git.meshkee.com:Meshkee/backend.git` with the VM deploy key registered as a **read-only deploy key** on the repo. Gitea SSH is on **port 222**. On the API VM, `/root/.ssh/config` must set `Port 222` for `Host git.meshkee.com` (system SSH on 22 is not Gitea). ## Useful commands ```bash pm2 status pm2 logs meshkee-api docker compose logs -f postgres ```