# Qlockify Deployment Main deployment and operations repository for Qlockify. This repo is the entrypoint for running the full product stack in production. ## Related Repositories - Deployment repository declared by `origin`: `https://git.amiirkhl.ir/Qlockify/qlockify-core-deployment.git` - Backend repository declared by its `origin`: `https://git.amiirkhl.ir/Qlockify/qlockify-backend-deployment.git` - Frontend repository declared by its `origin`: `https://git.amiirkhl.ir/Qlockify/qlockify-frontend-deployment.git` Use this repo for: - Docker Compose orchestration - Nginx - SSL certificate mounting - domain routing - environment layout - production service startup Use the backend and frontend repos for application-level implementation details. ## What This Repo Contains - `docker-compose.yml` - Nginx config - Postgres support files - Dockerfiles for production images - deployment environment samples - container networking and volume wiring ## Architecture Main deployed services: - `nginx` - `frontend` - `backend` - `celery` - `celery-beat` - `redis` - `db` Traffic pattern: - `qlockify.ir` serves the frontend - backend traffic is served from `qlockify.ir` under `/api` and `/admin` - Nginx terminates TLS and proxies requests to the frontend and backend containers ## Expected Repository Layout Docker builds read from nested application directories inside this repository: - `./backend/qlockify-backend-deployment` - `./frontend/qlockify-frontend-deployment` Expected layout: ```text qlockify-deployment/ backend/ Dockerfile .env.sample qlockify-backend-deployment/ frontend/ Dockerfile .env.sample qlockify-frontend-deployment/ nginx/ postgres/ docker-compose.yml ``` ## Deployment Flow ### 1. Place application source Put the app repos into: - `./backend/qlockify-backend-deployment` - `./frontend/qlockify-frontend-deployment` ### 2. Configure env files Create and fill: - `./.env` - `./backend/qlockify-backend-deployment/.env` - `./frontend/qlockify-frontend-deployment/.env` ### 3. Build and run ```powershell docker compose up -d --build ``` The backend container runs: - database migrations - `collectstatic` - Gunicorn startup ## Domain and Routing Configured domains: - `qlockify.ir` - `www.qlockify.ir` Behavior: - `www.qlockify.ir` redirects to `qlockify.ir` - `http` redirects to `https` - frontend is served from `qlockify.ir` - backend traffic is proxied from `qlockify.ir/api` and `qlockify.ir/admin` Before production startup: 1. Point DNS records for `qlockify.ir` and `www.qlockify.ir` to the server. 2. Make sure `80` and `443` are open on the server firewall. 3. Make sure the TLS certificate covers all required names. ## SSL Certificates Place certificate files here: ```text ./nginx/certs/fullchain.pem ./nginx/certs/privkey.pem ``` The repository intentionally keeps only: - `./nginx/certs/.gitkeep` Real certificate files are ignored by git. ## Required Backend Environment Set these in: ```text ./backend/qlockify-backend-deployment/.env ``` Core production values: - `DJANGO_ALLOWED_HOSTS=qlockify.ir,www.qlockify.ir` - `CORS_ALLOWED_ORIGINS=https://qlockify.ir,https://www.qlockify.ir` - `CSRF_TRUSTED_ORIGINS=https://qlockify.ir,https://www.qlockify.ir` - `BASE_URL=https://qlockify.ir` - `POSTGRES_HOST=db` - `REDIS_HOST=redis` - `REDIS_URL=redis://redis:6379/0` - `CELERY_BROKER_URL=redis://redis:6379/0` - `CELERY_RESULT_BACKEND=redis://redis:6379/1` Google OAuth values: - `GOOGLE_OAUTH_CLIENT_ID=...` - `GOOGLE_OAUTH_CLIENT_SECRET=...` - `GOOGLE_OAUTH_REDIRECT_URI=https://qlockify.ir/api/users/oauth/google/callback/` - `GOOGLE_OAUTH_FRONTEND_CALLBACK_URL=https://qlockify.ir/auth/google/callback` ## Required Frontend Environment Set this in: ```text ./frontend/qlockify-frontend-deployment/.env ``` ```text VITE_API_BASE_URL=/api ``` ## Background Workers This stack includes: - `celery` for async jobs - `celery-beat` for scheduled jobs If background scheduling stops working, inspect: ```powershell docker compose logs -f celery docker compose logs -f celery-beat ``` ## Notifications and SSE Notifications use Server-Sent Events at `/api/notifications/stream/`. Current behavior: - Nginx disables buffering for the SSE endpoint - Gunicorn is tuned to tolerate connected streams for current traffic - if concurrency grows materially, move SSE to async workers or a dedicated ASGI service ## Useful Operations Build/rebuild: ```powershell docker compose up -d --build ``` Restart a subset: ```powershell docker compose up -d --build nginx backend frontend ``` Inspect running services: ```powershell docker compose ps ``` Follow logs: ```powershell docker compose logs -f nginx docker compose logs -f backend docker compose logs -f celery docker compose logs -f celery-beat ``` Stop everything: ```powershell docker compose down ``` Backup the deployed data: ```bash chmod +x ./scripts/backup.sh ./scripts/backup.sh ``` By default, backup archives are written to `./backups/`. Each archive contains: - PostgreSQL data from the `db` service - media files from the Docker media volume - `.env` files from the deployment, backend, and frontend projects Restore a backup archive on another machine: ```bash chmod +x ./scripts/restore.sh ./scripts/restore.sh ./backups/qlockify-backup-YYYYMMDD-HHMMSS.tar.gz docker compose up -d --build ``` Restore is destructive for the database by default. To restore only part of an archive, use: ```bash RESTORE_SKIP_DB=1 ./scripts/restore.sh ./backups/qlockify-backup-YYYYMMDD-HHMMSS.tar.gz RESTORE_SKIP_MEDIA=1 ./scripts/restore.sh ./backups/qlockify-backup-YYYYMMDD-HHMMSS.tar.gz RESTORE_SKIP_ENV=1 ./scripts/restore.sh ./backups/qlockify-backup-YYYYMMDD-HHMMSS.tar.gz ``` Install backup upload prerequisites: ```bash sudo apt update sudo apt install -y rclone openssl ``` Configure encrypted S3-compatible backup uploads in `./.env`: ```bash S3_BACKUP_BUCKET=qlockify-backups S3_BACKUP_PREFIX=qlockify S3_BACKUP_ENDPOINT_URL=https://c284984.parspack.net S3_BACKUP_ACCESS_KEY_ID= S3_BACKUP_SECRET_ACCESS_KEY= BACKUP_ENCRYPTION_PASSPHRASE= BACKUP_LOCAL_KEEP_LATEST=3 BACKUP_REMOTE_KEEP_LATEST=7 ``` Upload an encrypted backup to S3-compatible object storage with rclone: ```bash chmod +x ./scripts/backup-upload-s3.sh ./scripts/backup-upload-s3.sh ``` After a successful upload, the remote storage keeps only the latest `BACKUP_REMOTE_KEEP_LATEST` encrypted `.tar.gz.enc` files. The server keeps only the latest `BACKUP_LOCAL_KEEP_LATEST` plaintext `.tar.gz` files under `./backups/latest/`; encrypted backup files are not kept locally. Restore the latest encrypted backup from S3: ```bash chmod +x ./scripts/restore-from-s3.sh ./scripts/restore-from-s3.sh latest docker compose up -d --build ``` Restore a specific encrypted backup from S3: ```bash ./scripts/restore-from-s3.sh qlockify/qlockify-backup-YYYYMMDD-HHMMSS.tar.gz.enc docker compose up -d --build ``` Schedule daily encrypted uploads with cron: ```cron 0 2 * * * cd /home/ubuntu/qlockify-deployment && ./scripts/backup-upload-s3.sh >> ./backups/backup.log 2>&1 ``` The S3 restore script supports the same partial restore flags: ```bash RESTORE_SKIP_DB=1 ./scripts/restore-from-s3.sh latest RESTORE_SKIP_MEDIA=1 ./scripts/restore-from-s3.sh latest RESTORE_SKIP_ENV=1 ./scripts/restore-from-s3.sh latest ``` Keep `BACKUP_ENCRYPTION_PASSPHRASE` in a safe place outside the server. Encrypted backups cannot be restored without it. ## CI/CD with Gitea Actions This repository now ships with a Gitea Actions deployment workflow in: - `.gitea/workflows/deploy.yml` The backend and frontend repositories each ship with their own workflow files: - backend: `.gitea/workflows/backend.yml` - frontend: `.gitea/workflows/frontend.yml` Deployment behavior: - backend repo push to `main`: runs backend CI, then updates the backend checkout on the server and rebuilds `backend`, `celery`, and `celery-beat` - frontend repo push to `main`: runs frontend lint/build, then updates the frontend checkout on the server and rebuilds `frontend` - deployment repo push to `main`: validates deployment files, then updates the deployment checkout on the server and rebuilds `nginx` plus the app services The remote deploy entrypoint is: - `./scripts/deploy.sh` ### One-Time Server Bootstrap Before Actions can deploy automatically, make sure the server is prepared once. 1. Clone all three repositories on the server into the expected layout: ```text ~/qlockify-deployment ~/qlockify-deployment/backend/qlockify-backend-deployment ~/qlockify-deployment/frontend/qlockify-frontend-deployment ``` 2. Make sure the server can `git fetch` all three repositories non-interactively. Recommended approach: - add a deploy SSH key on the server - add the public key to Gitea as a deploy key or a machine-user SSH key - switch the server-side git remotes to SSH URLs 3. Pull the latest deployment repo once so the server has `scripts/deploy.sh`. 4. Make the deploy script executable: ```bash chmod +x ~/qlockify-deployment/scripts/deploy.sh ``` 5. Make sure the deploy user can run Docker Compose on the server. ### Gitea Runner Setup Gitea Actions requires a trusted runner. Gitea's official docs describe the runner and label model here: - Actions overview: `https://docs.gitea.com/usage/actions/overview` - Act Runner: `https://docs.gitea.com/usage/actions/act-runner` Recommended label setup for this project: ```text qlockify-python:docker://python:3.14-bookworm qlockify-node:docker://node:22-bookworm qlockify-deploy:docker://ubuntu:24.04 ``` Example non-interactive runner registration: ```bash ./act_runner register \ --no-interactive \ --instance https://git.amiirkhl.ir \ --token \ --name qlockify-runner \ --labels "qlockify-python:docker://python:3.14-bookworm,qlockify-node:docker://node:22-bookworm,qlockify-deploy:docker://ubuntu:24.04" ``` Then start the runner daemon: ```bash ./act_runner daemon ``` ### Gitea Secrets Create these Actions secrets either at the `Qlockify` organization level or per repository. - `SSH_PRIVATE_KEY` - private key used by the workflow to SSH into the deployment server - `SSH_KNOWN_HOSTS` - output of: ```bash ssh-keyscan -H ``` Do not create `GITEA_TOKEN` manually. Gitea provides a built-in job token and exposes it as `${{ secrets.GITEA_TOKEN }}`. See: - `https://docs.gitea.com/usage/actions/token-permissions` ### Gitea Variables Create these Actions variables in the Gitea UI: - `DEPLOY_HOST` - `DEPLOY_PORT` - `DEPLOY_USER` - `DEPLOY_PATH` - `DEPLOY_BRANCH` - `BACKEND_BRANCH` - `FRONTEND_BRANCH` Suggested values for your current server layout: ```text DEPLOY_HOST=h9arjloaye DEPLOY_PORT=22 DEPLOY_USER=ubuntu DEPLOY_PATH=/home/ubuntu/qlockify-deployment DEPLOY_BRANCH=main BACKEND_BRANCH=main FRONTEND_BRANCH=main ``` Gitea variables are available in workflows through `${{ vars.NAME }}`. Gitea documents that here: - `https://docs.gitea.com/usage/actions/actions-variables` ### Recommended UI Setup If all three repositories live under the same `Qlockify` organization, the cleanest setup is: 1. Add one organization-level runner to `Qlockify` 2. Add organization-level variables for the shared deploy target 3. Add organization-level secrets for the SSH key and known hosts This keeps the three repositories consistent and avoids copying the same values three times. ### First Deploy Check After creating the runner, variables, and secrets: 1. push the deployment repository first 2. confirm the deployment workflow succeeds 3. then push backend or frontend changes and confirm their repo-specific workflows deploy only the affected services If a workflow fails on the server step, first check: - runner logs - repository Actions logs - `docker compose logs -f backend` - `docker compose logs -f frontend` - `docker compose logs -f celery` - `docker compose logs -f celery-beat` ## Scope Boundary This repo should document: - infrastructure - runtime topology - domains - Nginx - Docker Compose - SSL - operational startup and troubleshooting It should not duplicate the application-specific implementation details already documented in the backend and frontend repositories.