153 lines
6.8 KiB
Markdown
153 lines
6.8 KiB
Markdown
# Dyolink
|
||
|
||
Monorepo: **NestJS** backend (`backend/`), **Next.js** frontend (`frontend/`), **Docker** stack under `infrastructure/`..
|
||
|
||
Local development: see **`backend/README.md`** and **`frontend/README.md`**.
|
||
|
||
**Cursor AI:** project conventions for agents are in [`AGENTS.md`](AGENTS.md), [`.cursor/rules/`](.cursor/rules/), and [`.cursor/skills/`](.cursor/skills/).
|
||
|
||
---
|
||
|
||
## Production deploy (Docker Hub + HTTPS + Let's Encrypt)
|
||
|
||
**Full step-by-step guide:** [`infrastructure/DEPLOY.md`](infrastructure/DEPLOY.md)
|
||
|
||
Minimal server setup: install Docker, create `.env` + `secrets/`, `docker login`, run one script.
|
||
|
||
| On server (once) | In repo / Docker |
|
||
|------------------|------------------|
|
||
| DNS A record → server IP | `docker-compose.prod.yml`, nginx, certbot |
|
||
| `docker login` (private Hub) | Build & push images from dev machine |
|
||
| `secrets/database.env`, `secrets/backend.env` | Examples: `database.prod.env.example`, `backend.prod.env.example` |
|
||
| `infrastructure/.env` (`DOMAIN`, `LETSENCRYPT_EMAIL`) | `deploy.prod.env.example` |
|
||
|
||
**Dev machine** — build frontend with the public domain baked in, push to Docker Hub:
|
||
|
||
```bash
|
||
./infrastructure/scripts/build-and-push-prod.sh wixur.ir latest
|
||
```
|
||
|
||
**Server** — from `infrastructure/`:
|
||
|
||
```bash
|
||
cp deploy.prod.env.example .env # edit DOMAIN, paths
|
||
mkdir -p ../secrets && cp database.prod.env.example ../secrets/database.env
|
||
cp backend.prod.env.example ../secrets/backend.env # set passwords + FRONTEND_URL
|
||
docker login
|
||
chmod +x scripts/*.sh
|
||
./scripts/deploy-prod.sh
|
||
```
|
||
|
||
SSL is issued automatically via **Certbot** (`scripts/init-letsencrypt.sh`). Nginx config is generated from `DOMAIN` in `.env`. When you move to another domain (e.g. `dyolink.com`), update `.env` + `backend.env`, re-run `init-letsencrypt.sh`, and **rebuild the frontend image** with the new URL.
|
||
|
||
---
|
||
|
||
## Deploy on your own server (Docker + Gitea)
|
||
|
||
High level: **build container images → push to a registry → server pulls images and runs Compose**. Optionally **Gitea Actions** automates that on every merge to `main` / `master`.
|
||
|
||
### 1. One-time server preparation
|
||
|
||
1. Install **Docker** and **Docker Compose** on the server.
|
||
2. Run **Gitea** with the **container registry** enabled (same host/port you use for `docker login`, e.g. `178.131.50.201:3000`).
|
||
3. Copy the repo (or deploy only `infrastructure/` + secrets). You need at least:
|
||
|
||
- `infrastructure/docker-compose.registry.yml`
|
||
- `infrastructure/nginx/` configs referenced by that compose file
|
||
- `infrastructure/database/init.sql` if used by your Postgres service
|
||
|
||
4. **Secrets on the server** (never commit real values):
|
||
|
||
- Copy `infrastructure/database.staging.env.example` → **`database.staging.env`** (Postgres user/password/db).
|
||
- Copy `infrastructure/backend.staging.env.example` → **`backend.staging.env`** (e.g. `DATABASE_URL`, JWT, pointing at the compose Postgres service name).
|
||
- Put both files in one directory on the server, e.g. `/opt/dyolink/secrets/`.
|
||
|
||
5. **Registry login from the server** (same credentials you use for `docker push`):
|
||
|
||
```bash
|
||
docker login <registry-host>:<port> -u <user>
|
||
```
|
||
|
||
For HTTP registries, Docker may require **`insecure-registries`** on the daemon.
|
||
|
||
### 2. Manual deploy (build images elsewhere, run on server)
|
||
|
||
On your **dev machine** (after successful local builds):
|
||
|
||
```powershell
|
||
$REG = "<registry-host>:<port>"
|
||
$OWN = "<registry-owner>"
|
||
$TAG = "manual"
|
||
|
||
docker build -t "${REG}/${OWN}/dyolink-backend:${TAG}" -t "${REG}/${OWN}/dyolink-backend:latest" ./backend
|
||
|
||
docker build `
|
||
--build-arg NEXT_PUBLIC_API_URL="http://<your-public-ip>:<nginx-port>/api" `
|
||
--build-arg NEXT_PUBLIC_APP_URL="http://<your-public-ip>:<nginx-port>" `
|
||
--build-arg NEXT_PUBLIC_APP_NAME="Dyolink" `
|
||
-t "${REG}/${OWN}/dyolink-frontend:${TAG}" `
|
||
-t "${REG}/${OWN}/dyolink-frontend:latest" `
|
||
./frontend
|
||
|
||
docker push "${REG}/${OWN}/dyolink-backend:${TAG}"
|
||
docker push "${REG}/${OWN}/dyolink-backend:latest"
|
||
docker push "${REG}/${OWN}/dyolink-frontend:${TAG}"
|
||
docker push "${REG}/${OWN}/dyolink-frontend:latest"
|
||
```
|
||
|
||
On the **server**, from `infrastructure/`:
|
||
|
||
1. Create **`deploy.registry.env`** (see `infrastructure/deploy.registry.env.example`):
|
||
|
||
- `REGISTRY_PREFIX=<host>:<port>/<owner>` (no `http://`, no trailing slash)
|
||
- `IMAGE_TAG=latest` or the tag you pushed
|
||
- `STAGING_HTTP_PORT=<host port>` (e.g. `8088` — browser uses `http://<ip>:8088`)
|
||
|
||
2. Set **`DEPLOY_SECRETS_DIR`** to the absolute path of the folder containing `database.staging.env` and `backend.staging.env` (you can export it in the shell or add it to `deploy.registry.env` if your Compose setup expects it).
|
||
|
||
3. Pull and start:
|
||
|
||
```bash
|
||
docker compose -f docker-compose.registry.yml --env-file deploy.registry.env pull backend frontend
|
||
docker compose -f docker-compose.registry.yml --env-file deploy.registry.env up -d
|
||
```
|
||
|
||
The backend container runs **`prisma migrate deploy`** on startup (via entrypoint) when `NODE_ENV=production`, so schema updates apply after you deploy a new image that includes new migrations.
|
||
|
||
### 3. Automatic deploy (Gitea Actions)
|
||
|
||
Workflow file: **`.gitea/workflows/registry-build-deploy.yml`**.
|
||
|
||
**Requirements:**
|
||
|
||
- **Gitea Actions** enabled for the repository.
|
||
- A **self-hosted runner** (with Docker) registered to Gitea — the workflow uses `runs-on: self-hosted`.
|
||
- **Windows runners:** the workflow uses **PowerShell** (not Bash). Gitea’s runner was failing with `execvpe(/bin/bash) failed` when Bash was routed through WSL without a real `/bin/bash`. If your runner is **Linux**, switch `.gitea/workflows/registry-build-deploy.yml` to `defaults.run.shell: bash` and use Bash syntax instead.
|
||
- **Repository → Actions → Variables** (examples):
|
||
|
||
- `REGISTRY_HOST` — e.g. `178.131.50.201:3000`
|
||
- `REGISTRY_OWNER` — image namespace (same as Docker image path after the host), e.g. `admin`
|
||
- `PUBLIC_BASE_URL` — URL users open in the browser, e.g. `http://178.131.50.201:8088` (no trailing slash)
|
||
- `DEPLOY_SECRETS_DIR` — **absolute path on the runner machine** to the folder containing `database.staging.env` and `backend.staging.env`
|
||
- Optional: `STAGING_HTTP_PORT` (defaults to `8088`)
|
||
|
||
- **Repository → Actions → Secrets:**
|
||
|
||
- `REGISTRY_USERNAME`
|
||
- `REGISTRY_PASSWORD` — access token with package read/write (or equivalent)
|
||
|
||
**Trigger:** push to **`main`** or **`master`**, or run the workflow manually (**workflow_dispatch**).
|
||
|
||
The pipeline clones from your Gitea instance, builds and pushes backend/frontend images, then on the runner runs **`docker compose pull`** and **`up -d`** using `infrastructure/docker-compose.registry.yml`.
|
||
|
||
---
|
||
|
||
## Related paths
|
||
|
||
| Path | Role |
|
||
|------|------|
|
||
| `backend/Dockerfile` | API image |
|
||
| `frontend/Dockerfile` | Web image |
|
||
| `infrastructure/docker-compose.registry.yml` | Pull-only staging stack (registry images + nginx + postgres) |
|
||
| `infrastructure/deploy.registry.env.example` | Template for `deploy.registry.env` |
|