# NODE-Cloud Server — Master Context for AI Assistants > Last updated: 2026-04-27 > Owner: Ali Taghavi (@biztaghavi), CEO of NODE-Group (Novin Ofogh Dadeh Etemad) > Purpose: Paste this into any new Claude/AI chat when working on this server or deploying new apps. --- ## 1. Server Overview | Field | Value | |---|---| | Hostname | `srv9588446167` | | IP | `193.105.234.35` | | OS | Ubuntu 24.04.4 LTS | | Kernel | 6.8.0-107-generic | | CPU | 4 cores (KVM/QEMU) | | RAM | 3.8 GB | | Disk | 145 GB (29 GB used, 116 GB free) | | Swap | None | | Virtualization | KVM | | SSH | Port 22, root access | | Docker | 28.2.2 with Compose 2.37.1 | | Nginx | 1.24.0 | | Certbot | 2.9.0 with auto-renewal timer active | --- ## 2. Iran-Specific Constraints (CRITICAL — Read Before Any Deployment) This server is hosted in Iran. Several international services are blocked or unreliable: ### 2.1 Docker Images - **Docker Hub is blocked.** All images must use the ArvanCloud mirror: `docker.arvancloud.ir/library/` - Examples: `docker.arvancloud.ir/library/node:20-alpine`, `docker.arvancloud.ir/library/mariadb:11`, `docker.arvancloud.ir/library/redis:7-alpine` - Docker is configured with mirror: `https://docker-mirror.liara.ir/` ### 2.2 npm/Node.js Packages - **npmjs.org is intermittently blocked.** Use Liara's npm mirror: `https://package-mirror.liara.ir/repository/npm/` - **npm v10 has a known bug** ("Exit handler never called") — it silently fails and doesn't install packages. Use **pnpm** instead. - **pnpm installation:** Use `corepack enable && corepack prepare pnpm@9 --activate` with `ENV COREPACK_NPM_REGISTRY=https://package-mirror.liara.ir/repository/npm/` - **Liara mirror rate-limits aggressively** — use `--network-concurrency 1` to avoid 429 errors. If rate-limited, wait 10-30 minutes and retry. - **Fallback strategy for large projects:** Install `node_modules` on a Mac/dev machine with unrestricted internet, then `scp` the tarball to the server. When doing this: - Install with `npm install --platform=linux --arch=x64 --libc=musl` to get Linux Alpine binaries - Also manually install native musl binaries: `@next/swc-linux-x64-musl`, `lightningcss-linux-x64-musl`, `@tailwindcss/oxide-linux-x64-musl` - Match SWC version to the exact Next.js version (check `package.json`) - Set `ENV NEXT_SWC_DOWNLOAD_DISABLED=1` in Dockerfile to prevent Next.js from trying to download SWC ### 2.3 Alpine Linux Packages - **`apk add` is blocked** — `dl-cdn.alpinelinux.org` is unreachable. Never use `apk add` in Dockerfiles. The base `node:20-alpine` image already has what's needed. ### 2.4 Google Services - **Google Fonts are blocked at build time.** If a project uses `next/font/google`, it must be replaced with `next/font/local` or `@fontsource/*` local packages. - `fonts.googleapis.com` is unreachable during Docker builds. ### 2.5 Prisma ORM - **`binaries.prisma.sh` is blocked.** Prisma cannot download its engine binaries during build. - **Solution:** Pre-download Prisma engine binaries on a machine with internet access, commit them to the repo in `prisma-binaries/`, and set these env vars in the Dockerfile: ```dockerfile ENV PRISMA_QUERY_ENGINE_LIBRARY=/app/prisma-binaries/libquery_engine-linux-musl-openssl-3.0.x.so.node ENV PRISMA_SCHEMA_ENGINE_BINARY=/app/prisma-binaries/schema-engine-linux-musl-openssl-3.0.x ENV PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1 ENV PRISMA_SKIP_POSTINSTALL_GENERATE=1 ``` - Both `libquery_engine` and `schema-engine` binaries are needed for `linux-musl-openssl-3.0.x` (Alpine). - Run `prisma generate` in the Dockerfile after copying binaries — it uses local files, no network needed. - For migrations, apply SQL files directly: `docker exec -i mariadb -u user -ppassword dbname < migration.sql` ### 2.6 MySQL - **MySQL 8.0+ Docker images crash** on this server's CPU (no x86-64-v2 support). Use **MariaDB 11** instead — fully MySQL-compatible. - MariaDB healthcheck: `["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]` ### 2.7 SSL / Let's Encrypt - **Let's Encrypt cannot reach this server directly** for domains behind ArvanCloud CDN. Certbot fails with timeouts. - **For domains behind ArvanCloud CDN:** Use ArvanCloud's built-in SSL (set mode to "Full" or "Flexible"). No certbot needed. - **For domains pointing directly to the server IP:** Certbot works fine (e.g., pouyatebparto.ir, nodegroup.ir, hooranhr.ir). - A **self-signed certificate** exists at `/etc/ssl/certs/nodecloud-selfsigned.crt` and `/etc/ssl/private/nodecloud-selfsigned.key` — used for domains behind ArvanCloud where ArvanCloud handles public SSL. - **IMPORTANT:** Any new site behind ArvanCloud CDN MUST listen on port 443 with the self-signed cert in Nginx. Otherwise Nginx falls back to the first server block that has SSL (which is git.nodecloud.ir/Gitea) and serves wrong content. --- ## 3. Hosted Applications ### 3.1 Gitea (Self-hosted Git) | Field | Value | |---|---| | Domain | `git.nodecloud.ir` | | Container | `gitea` + `gitea-db` | | Port | `127.0.0.1:3000` | | Stack | Gitea 1.25.5 + PostgreSQL 14 | | Compose | `/srv/nodecloud/gitea/docker-compose.yml` | | SSL | Self-signed cert (ArvanCloud CDN handles public SSL) | ### 3.2 Pouya Teb Parto (Medical Services) | Field | Value | |---|---| | Domain | `pouyatebparto.ir` | | Container | `pouya_teb_app` + `pouya_teb_mysql` + `pouya_teb_phpmyadmin` | | Port | `127.0.0.1:3001` | | Stack | Next.js 16 + MariaDB 11 + phpMyAdmin | | Compose | `/srv/nodecloud/apps/pouya-teb-parto/docker-compose.prod.yml` | | Git | `http://git.nodecloud.ir/pouyadolat/Pouya-teb-parto.git` | | SSL | Let's Encrypt (certbot) | | phpMyAdmin | `127.0.0.1:8080` (SSH tunnel only) | ### 3.3 NODE-Group Website | Field | Value | |---|---| | Domain | `nodegroup.ir` | | Container | `nodegroup_web` | | Port | `127.0.0.1:3002` | | Stack | Static Next.js (exported to HTML, served by Nginx in container) | | Compose | `/srv/nodecloud/apps/nodegroup/docker-compose.prod.yml` | | Git | `http://git.nodecloud.ir/nodegroup/nodegroupirwebsite.git` | | SSL | Let's Encrypt (certbot) | ### 3.4 Hooran HR (HR Gamification Platform) | Field | Value | |---|---| | Domain | `hooranhr.ir`, `www.hooranhr.ir`, `hooranhr.com`, `www.hooranhr.com` | | Container | `hooranhr_app` + `hooranhr_mysql` | | Port | `127.0.0.1:3003` | | Stack | Next.js 16 + MariaDB 11 + DISC assessment | | Compose | `/srv/nodecloud/apps/hooranhr/docker-compose.prod.yml` | | Git | `http://git.nodecloud.ir/nodegroup/hooranhrir.git` | | SSL | Let's Encrypt (certbot, covers all 4 domains) | ### 3.5 Khanehban (Building Management SaaS) | Field | Value | |---|---| | Domain | `khanehbaan.ir` | | Container | `khanehbaan-app-1` + `khanehbaan-mysql-1` + `khanehbaan-redis-1` | | Port | `127.0.0.1:3008` | | Stack | Next.js 16 + MariaDB 11 + Redis 7 + Prisma ORM | | Compose | `/srv/nodecloud/apps/khanehbaan/docker-compose.prod.yml` | | Git | `http://git.nodecloud.ir/nodegroup/khanehbaan-ir.git` | | SSL | ArvanCloud CDN (self-signed cert on server, ArvanCloud "Full" mode) | | S3 Storage | Liara S3 (`storage.c2.liara.space`, bucket: `buildingmanagement`) — remains on Liara | | Special | Prisma with pre-downloaded binaries, node_modules transferred from Mac | ### 3.6 Portainer (Docker Management UI) | Field | Value | |---|---| | Domain | `manage.nodecloud.ir` | | Container | `portainer` | | Port | `127.0.0.1:9000` | | Stack | Portainer CE LTS | | Compose | `/srv/nodecloud/apps/portainer/docker-compose.yml` | | SSL | Self-signed cert (ArvanCloud CDN handles public SSL) | ### 3.7 Nodecloud.ir (Gateway/Placeholder) | Field | Value | |---|---| | Domain | `nodecloud.ir`, `www.nodecloud.ir` | | Type | Static text response ("nodecloud main gateway is up") | | Port | 80 only (no SSL, no proxy) | --- ## 4. Port Allocation Map | Port | Service | Notes | |---|---|---| | 3000 | Gitea | Reserved | | 3001 | Pouya Teb Parto | Next.js app | | 3002 | NODE-Group website | Static Nginx | | 3003 | Hooran HR | Next.js app | | 3004-3007 | Available | — | | 3008 | Khanehban | Next.js + Prisma | | 3009+ | Available | — | | 8080 | phpMyAdmin (Pouya Teb) | SSH tunnel only | | 9000 | Portainer | Docker management UI | **Next available port: 3004** --- ## 5. Architecture Pattern for New Deployments Every app follows this pattern: ``` Internet → ArvanCloud CDN (SSL) → Server:80/443 → Nginx → 127.0.0.1:PORT → Docker Container ``` ### 5.1 Directory Structure ``` /srv/nodecloud/ ├── gitea/ # Gitea (separate, not under apps/) │ └── docker-compose.yml └── apps/ ├── pouya-teb-parto/ # Each app gets its own directory │ ├── docker-compose.prod.yml │ ├── Dockerfile │ ├── .env │ └── ... (source code) ├── nodegroup/ ├── hooranhr/ ├── khanehbaan/ └── portainer/ ``` ### 5.2 Standard Dockerfile Template (Next.js + pnpm) ```dockerfile FROM docker.arvancloud.ir/library/node:20-alpine AS builder WORKDIR /app ENV NEXT_TELEMETRY_DISABLED=1 ENV PATH="/app/node_modules/.bin:$PATH" ENV COREPACK_NPM_REGISTRY=https://package-mirror.liara.ir/repository/npm/ RUN corepack enable && corepack prepare pnpm@9 --activate COPY package.json ./ RUN pnpm config set registry https://package-mirror.liara.ir/repository/npm/ && \ pnpm install --no-frozen-lockfile --network-concurrency 1 COPY . . RUN next build FROM docker.arvancloud.ir/library/node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV PORT=3000 ENV HOSTNAME="0.0.0.0" RUN addgroup --system --gid 1001 nodejs && \ adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 CMD ["node", "server.js"] ``` > **Note:** `next.config` must have `output: "standalone"` for this to work. ### 5.3 Standard docker-compose.prod.yml Template ```yaml services: app: build: . container_name: APPNAME_app restart: unless-stopped ports: - "127.0.0.1:PORT:3000" env_file: .env environment: NODE_ENV: production # Only if app needs a database: mysql: image: docker.arvancloud.ir/library/mariadb:11 container_name: APPNAME_mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} volumes: - mysql_data:/var/lib/mysql healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 10s timeout: 5s retries: 20 # Only if app needs Redis: redis: image: docker.arvancloud.ir/library/redis:7-alpine restart: unless-stopped command: redis-server --appendonly yes volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 volumes: mysql_data: redis_data: ``` ### 5.4 Nginx Config Template **For domains with direct DNS (certbot possible):** ```nginx server { listen 80; listen [::]:80; server_name DOMAIN.ir www.DOMAIN.ir; client_max_body_size 20M; location / { proxy_pass http://127.0.0.1:PORT; 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; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 60; proxy_send_timeout 60; } } ``` Then: `certbot --nginx -d DOMAIN.ir -d www.DOMAIN.ir` **For domains behind ArvanCloud CDN (MUST include SSL block):** ```nginx server { listen 80; listen [::]:80; listen 443 ssl; listen [::]:443 ssl; server_name DOMAIN.ir www.DOMAIN.ir; ssl_certificate /etc/ssl/certs/nodecloud-selfsigned.crt; ssl_certificate_key /etc/ssl/private/nodecloud-selfsigned.key; client_max_body_size 20M; location / { proxy_pass http://127.0.0.1:PORT; 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; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 60; proxy_send_timeout 60; } } ``` In ArvanCloud: SSL mode = "Full", origin = `193.105.234.35`, purge cache after setup. --- ## 6. Deployment Checklist for New Sites 1. **Clone repo** to `/srv/nodecloud/apps/APPNAME/` 2. **Fix Dockerfile** for Iran: replace `node:20-alpine` with `docker.arvancloud.ir/library/node:20-alpine`, remove any `apk add`, use pnpm with Liara mirror 3. **Fix Google Fonts** if present: grep for `next/font/google` and replace 4. **Create `.env`** from `.env.example` with real credentials 5. **Use MariaDB 11** instead of MySQL 8.x 6. **Set port** in `docker-compose.prod.yml` to next available (currently 3004+) 7. **Build:** `docker compose -f docker-compose.prod.yml build --no-cache` 8. **Launch:** `docker compose -f docker-compose.prod.yml up -d` 9. **Verify:** `curl -s http://127.0.0.1:PORT | head -5` 10. **Run migrations** if needed (apply SQL directly to MariaDB container) 11. **Create Nginx config** in `/etc/nginx/sites-available/`, symlink to `sites-enabled/` 12. **SSL:** Either certbot (direct DNS) or self-signed + ArvanCloud CDN 13. **Test:** `curl -I https://DOMAIN` --- ## 7. Common Operations ### Redeploy an app after code changes ```bash cd /srv/nodecloud/apps/APPNAME git pull # Re-fix Dockerfile if git pull overwrote it docker compose -f docker-compose.prod.yml build --no-cache docker compose -f docker-compose.prod.yml up -d ``` ### View all container status ```bash docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" ``` ### View resource usage ```bash docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" ``` ### View logs for an app ```bash cd /srv/nodecloud/apps/APPNAME docker compose -f docker-compose.prod.yml logs -f app ``` ### Restart an app ```bash cd /srv/nodecloud/apps/APPNAME docker compose -f docker-compose.prod.yml restart ``` ### Access phpMyAdmin (Pouya Teb) via SSH tunnel From your Mac: ```bash ssh -L 8080:127.0.0.1:8080 root@193.105.234.35 ``` Then open `http://localhost:8080` ### Full server health check ```bash echo "=== CONTAINERS ===" && docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" && echo "" && echo "=== RESOURCES ===" && docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" ``` --- ## 8. Firewall (UFW) Only three ports are open: - **22** (SSH) - **80** (HTTP) - **443** (HTTPS) All Docker container ports are bound to `127.0.0.1` — not publicly accessible. Nginx is the only public-facing service. --- ## 9. Known Issues & Gotchas 1. **git pull overwrites Dockerfile** — After pulling, always re-apply Iran-specific fixes (image mirrors, apk removal, pnpm config). Consider committing a `Dockerfile.prod` to the repo instead. 2. **No swap configured** — With 3.8 GB RAM and ~1.8 GB used, there's ~2 GB headroom. Adding more apps may require adding swap or upgrading RAM. 3. **DNS resolver on server is slow** — `dig` commands sometimes time out. This doesn't affect app operation, only manual DNS lookups from the server. 4. **ArvanCloud CDN caching** — After any Nginx config change, always purge ArvanCloud cache and test in incognito. 5. **Self-signed cert is shared** — `git.nodecloud.ir`, `manage.nodecloud.ir`, and `khanehbaan.ir` all use the same self-signed cert. This is fine because ArvanCloud handles public SSL. 6. **Certbot auto-renewal** is active but will only renew certs for domains with direct DNS (not behind ArvanCloud CDN). 7. **No automated backups** — Docker volumes for databases are not backed up. Consider setting up periodic `mysqldump` cron jobs. 8. **Portainer times out** if admin account isn't created quickly after restart. Run `docker restart portainer` and immediately access the UI.