# 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 <mysql_container> 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.
