# Coolify deployment The API and the web UI are two Git repositories. Each one is a Dockerfile application. MongoDB is a Coolify database resource. Nothing in this layout is a shared production Compose file. `docker-compose.yml` in the backend repository is the local API stack (Mongo + API). Coolify should deploy each repository from its own root, not from a monorepo subdirectory. Checked against the current Coolify docs: Base Directory is the build context, and Dockerfile Location is relative to that directory. The form shows the joined path. ## Applications ### Frontend — https://kamidso.com | Field | Value | | --- | --- | | Repository | frontend repository | | Branch | `main` | | Build pack / strategy | Dockerfile | | Base Directory | `/` | | Dockerfile Location | `/Dockerfile` | | Joined path shown by Coolify | `/Dockerfile` | | Ports Exposes | `80` | | Domain | `https://kamidso.com` | | Watch Paths | `**` | The frontend repository is the whole build context. Build-time variables. In Environment Variables set Build time to **Available during build** and Runtime to **Not available in the container**. Create a variable only when it has a value. An empty value overrides the Dockerfile default. | Name | Value | | --- | --- | | `VITE_API_BASE_URL` | `https://api.kamidso.com` | | `NGINX_CONF` | `nginx.prod.conf` | `VITE_API_BASE_URL` is inlined by Vite. Changing it in the running container does nothing until the next image build. It is declared after `npm ci`, so changing it does not reinstall npm packages. `nginx.prod.conf` serves the SPA only. The browser calls `api.kamidso.com` itself. That keeps the session cookie on the API host. The local Compose file does not set `NGINX_CONF`, so it still uses `nginx.conf` and proxies `/api` to the `backend` service. Optional, only if a mirror is required: `NPM_REGISTRY`, `NODE_IMAGE`, `NGINX_IMAGE`. See Mirrors. ### Backend — https://api.kamidso.com | Field | Value | | --- | --- | | Repository | backend repository | | Branch | `main` | | Build pack / strategy | Dockerfile | | Base Directory | `/` | | Dockerfile Location | `/Dockerfile` | | Joined path shown by Coolify | `/Dockerfile` | | Ports Exposes | `5670` | | Domain | `https://api.kamidso.com` | | Watch Paths | `**` | The image command is the one already in the Dockerfile: ```text python -m kamidso.cli serve --host 0.0.0.0 --port 5670 ``` Do not replace it with a guessed uvicorn line. Leave Ports Mapped empty so `5670` is not published on the host. The proxy reaches the container on the Docker network. Persistent storage: one volume mounted at `/app/data`. Engine SQLite and Chroma paths are under that directory (`data/meta`, `data/vectors`). Set `KAMIDSO_STORAGE_PATH=/app/data`. The image health check is `GET /health/live` on port `5670`, start period 180 seconds. `/health/ready` reports Mongo and the engine and can stay `degraded` while those are still coming up, so it is the wrong deploy gate. If Coolify also has a health check, use the same live path and a start period of at least 180 seconds. Runtime variables. Set Build time to **Not available during build**. Secrets must not be build arguments. | Name | Value | | --- | --- | | `KAMIDSO_ENV` | `production` | | `KAMIDSO_DEVELOPER_MODE` | `false` | | `OTP_MOCK_ENABLED` | `false` | | `KAMIDSO_HOST` | `0.0.0.0` | | `KAMIDSO_PORT` | `5670` | | `KAMIDSO_ENGINE_CONFIG` | `configs/dbgpt-proxy-openai.toml` | | `KAMIDSO_STORAGE_PATH` | `/app/data` | | `KAMIDSO_DEFAULT_LOCALE` | `fa-IR` | | `DBGPT_LANG` | `fa` | | `DBGPT_CORS_ALLOWED_ORIGINS` | `https://kamidso.com,https://www.kamidso.com` | | `LLM_MODEL_PROVIDER` | `proxy/openai` | | `EMBEDDING_MODEL_PROVIDER` | `proxy/openai` | | `MONGODB_URI` | internal URL from the Coolify database resource | | `KAMIDSO_JWT_SECRET` | secret, runtime only | | `KAMIDSO_ADMIN_EMAIL` | secret, runtime only | | `KAMIDSO_ADMIN_PASSWORD` | secret, runtime only | | `ENGINE_ENCRYPT_KEY` | secret, runtime only | | `OPENAI_API_KEY` | secret, runtime only | | `OPENAI_API_BASE` | model endpoint, runtime only | | `LLM_MODEL_NAME` | runtime only | | `EMBEDDING_MODEL_NAME` | runtime only | | `EMBEDDING_MODEL_API_URL` | runtime only | | `KAVENEGAR_API_KEY` | secret, runtime only | | `OPENROUTER_PROXY_URL` | see Egress | `KAMIDSO_ENV=production` locks product CORS to `https://kamidso.com` and `https://www.kamidso.com`. The engine reads `DBGPT_CORS_ALLOWED_ORIGINS` separately; leave it as that same pair so it does not fall back to `*`. The session cookie is host-only (`Domain` is not set), `HttpOnly`, `SameSite=Lax`, and `Secure` when `KAMIDSO_ENV=production`. It is stored for `api.kamidso.com`. Streaming stays on that host: the SPA does not proxy SSE through nginx. Optional build-time variables, only when a mirror is required: `PIP_INDEX_URL`, `PIP_TRUSTED_HOST`, `DEBIAN_MIRROR`, `DEBIAN_SECURITY_MIRROR`, `PYTHON_IMAGE`. Build time available, runtime not available. ## Cache settings on both applications Configuration → Advanced → Build: | Setting | Value | | --- | --- | | Build cache | **Use Docker build cache** | | Build arguments | **Managed manually in Dockerfile** | | Source commit availability | **Runtime only (preserves cache)** | Automatic injection writes an `ARG` line for every build-time variable at the top of the Dockerfile and drops the dependency-layer cache whenever any of those values change. Manual mode keeps the `ARG` positions in the Dockerfiles. Ordinary deploys use the normal Deploy button. **Force deploy (without cache)** rebuilds apt, pip, and npm from scratch. Use it only when a layer is known to be wrong. On the Coolify server, BuildKit's own garbage collection can still delete this cache. `/etc/docker/daemon.json`: ```json { "builder": { "gc": { "enabled": true, "defaultKeepStorage": "20GB" } } } ``` Then `systemctl restart docker`. Pick a size that fits the disk. The backend image's dependency layer is large; a small cap evicts it between deploys. ## MongoDB Create a Coolify database resource. Do not install MongoDB in the backend image, and do not publish its port. 1. Same server and the same destination network as the backend. The default destination network is `coolify`. Dockerfile applications join that network, so the database hostname resolves. 2. Leave the public port unset. 3. Copy the resource's internal URL into the backend's `MONGODB_URI`. 4. The URL contains the user and password. It lives only in Coolify's environment variables. `docker/kamidso/init-app-user.sh` is a local init script. Coolify creates the database user itself. The script is not an input to the image. ## Egress `OPENROUTER_PROXY_URL` is runtime configuration. The Dockerfile does not set `HTTP_PROXY` on the running container. Production OpenRouter traffic uses `http://egress-gateway:10809` on the external network `vira-egress`. A Dockerfile application has one destination network. Custom Docker Options cannot add `--network`; Coolify drops that flag. Put the egress gateway on the same destination network as the backend and keep `OPENROUTER_PROXY_URL=http://egress-gateway:10809`. If the gateway stays only on `vira-egress`, point `OPENROUTER_PROXY_URL` at an address the backend can already reach. Leave the variable empty only when OpenRouter is reachable directly. ## Mirrors Defaults are Docker Hub, `deb.debian.org`, `https://pypi.org/simple`, and `https://registry.npmjs.org`. Nothing in the Dockerfiles pins an Iranian mirror. Set the variables below on the Coolify application when the production server cannot use the upstream quickly. These URLs were requested from this network on 2026-10-09. A `200` means the package index or archive responded with real content. A Docker registry `401` on `/v2/` is the normal unauthenticated response, not a dead host. | Use | Primary | What responded | Fallback | | --- | --- | --- | --- | | PyPI | `https://package-mirror.liara.ir/repository/pypi/simple` | `200`, pip package links | `https://mirror-pypi.runflare.com/simple` | | npm | `https://package-mirror.liara.ir/repository/npm/` | `200`, `react` metadata | `https://mirror-npm.runflare.com` | | Debian | `https://linux-mirror.liara.ir/repository/debian` | `200`, `bookworm` Release | leave `DEBIAN_MIRROR` unset | | Debian security | `https://linux-mirror.liara.ir/repository/debian-security` | `200`, `bookworm-security` InRelease | leave `DEBIAN_SECURITY_MIRROR` unset | | Docker Hub pull-through | `https://docker-mirror.liara.ir` | `401` on `/v2/` | `https://docker.arvancloud.ir` | Runflare's free mirror quota is limited per IP. If it starts refusing requests, switch that variable back to Liara or to the official index. Preferred Docker Hub setup is a daemon registry mirror, because the image name stays `python:3.12-slim-bookworm` and the layer cache key does not change: ```json { "registry-mirrors": ["https://docker-mirror.liara.ir"] } ``` If that host fails, replace it with `https://docker.arvancloud.ir`. If both fail, remove `registry-mirrors` and pull from Docker Hub. Restart Docker after editing `daemon.json`. This can sit next to the BuildKit `builder.gc` block in the same file. `PYTHON_IMAGE`, `NODE_IMAGE`, and `NGINX_IMAGE` are the fallback when the server cannot set a registry mirror. Setting them changes the cache key. Example: `PYTHON_IMAGE=python:3.12-slim-bookworm` is already the default; only set the variable to a different name when you must. Backend apt and pip, when Liara is the chosen mirror: ```text DEBIAN_MIRROR=https://linux-mirror.liara.ir/repository/debian DEBIAN_SECURITY_MIRROR=https://linux-mirror.liara.ir/repository/debian-security PIP_INDEX_URL=https://package-mirror.liara.ir/repository/pypi/simple ``` `PIP_TRUSTED_HOST` is unnecessary for these HTTPS indexes. Set it only if pip reports a certificate error, and then only to that host. Frontend: ```text NPM_REGISTRY=https://package-mirror.liara.ir/repository/npm/ ``` Alpine packages are already inside `node:22-alpine` and `nginx:1.27-alpine`. The frontend Dockerfile does not run `apk add`. An Alpine mirror would not speed this build. Liara's Alpine mirror did respond (`v3.21` APKINDEX `200`) if a later change installs Alpine packages. ## Dependency install Backend install input is `requirements.txt`, copied before the source tree. `pyproject.toml` is local tooling and is not an install input. The image does not use `uv sync`. Liara and Docker both install from `requirements.txt`. The application is imported from `/app`. It is not an editable install, so a source change does not reinstall third-party packages. Frontend install input is `package.json` and `package-lock.json`, then `npm ci`. npm 12.2.0 is installed first because npm 10.9.9 in `node:22-alpine` crashes while building the tree. Source and `VITE_API_BASE_URL` come after that layer. pip and npm download caches use BuildKit cache mounts (`RUN --mount=type=cache`). Those mounts speed a rebuild of the install layer. They are not a substitute for the layer cache above. The Dockerfiles do not start with `# syntax=docker/dockerfile`, because that line pulls `docker/dockerfile` from Docker Hub before any other step. Coolify's BuildKit already accepts cache mounts. A log line that says `--mount` is unknown means that server's Docker is too old for this Dockerfile. ## What a normal deploy should show Source-only push: `npm ci` / `pip install -r requirements.txt` lines are `CACHED`. The `COPY` of the source and the Vite build (frontend) or the final `COPY . .` (backend) run again. `requirements.txt`, `package.json`, or `package-lock.json` changed: the install layer runs again. The apt layer stays cached unless the base image or the Debian mirror args changed. No file in the watch path changed: Coolify does not start a deployment. A manual deploy with a warm cache also skips the install when those files are unchanged.