Compare commits

...

12 Commits

11 changed files with 525 additions and 168 deletions

5
.gitignore vendored
View File

@@ -1,3 +1,8 @@
.env .env
backend/logs/ backend/logs/
backend/.pytest_cache/ backend/.pytest_cache/
backend/qlockify-backend-deployment
frontend/qlockify-frontend-deployment
nginx/certs/*
!nginx/certs/.gitkeep

253
README.md
View File

@@ -1,33 +1,250 @@
# Qlockify Deployment # Qlockify Deployment
This repository is the deployment layer only. Main deployment and operations repository for Qlockify.
It does not contain application source copies. Docker builds read directly from the sibling repositories: This repo is the entrypoint for running the full product stack in production.
- `../qlockify-backend` ## Related Repositories
- `../qlockify-frontend`
## Local structure - 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`
The expected directory layout is: 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
- `api.qlockify.ir` serves the backend API, admin, docs, static, and media
- 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 ```text
Qlockify/ qlockify-deployment/
qlockify-backend/ backend/
qlockify-frontend/ Dockerfile
qlockify-deployment/ .env.sample
qlockify-backend-deployment/
frontend/
Dockerfile
.env.sample
qlockify-frontend-deployment/
nginx/
postgres/
docker-compose.yml
``` ```
## Deployment flow ## Deployment Flow
1. Configure deployment env files: ### 1. Place application source
- `./.env`
- `./backend/.env` Put the app repos into:
- `./frontend/.env`
2. From `qlockify-deployment`, build and start the stack: - `./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 ```powershell
docker compose up --build docker compose up -d --build
``` ```
The backend container runs database migrations and `collectstatic` on startup, then serves Django with Gunicorn using `config.wsgi:application`. The backend container runs:
- database migrations
- `collectstatic`
- Gunicorn startup
## Domain and Routing
Configured domains:
- `qlockify.ir`
- `www.qlockify.ir`
- `api.qlockify.ir`
Behavior:
- `www.qlockify.ir` redirects to `qlockify.ir`
- `http` redirects to `https`
- frontend is served from `qlockify.ir`
- backend traffic is served from `api.qlockify.ir`
Before production startup:
1. Point DNS records for `qlockify.ir`, `www.qlockify.ir`, and `api.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=api.qlockify.ir,qlockify.ir,www.qlockify.ir`
- `CORS_ALLOWED_ORIGINS=https://qlockify.ir,https://www.qlockify.ir`
- `CSRF_TRUSTED_ORIGINS=https://api.qlockify.ir,https://qlockify.ir,https://www.qlockify.ir`
- `BASE_URL=https://api.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://api.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=https://api.qlockify.ir/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
```
## 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.

View File

@@ -2,10 +2,10 @@
ENVIRONMENT=development ENVIRONMENT=development
DEBUG=True DEBUG=True
# Django Core # Django Core
DJANGO_SETTINGS_MODULE=config.settings DJANGO_SETTINGS_MODULE=config.settings
DJANGO_SECRET_KEY= DJANGO_SECRET_KEY=
DJANGO_ALLOWED_HOSTS= DJANGO_ALLOWED_HOSTS=api.qlockify.ir,qlockify.ir,www.qlockify.ir
# Database # Database
POSTGRES_DB=app_db POSTGRES_DB=app_db
@@ -14,9 +14,9 @@ POSTGRES_PASSWORD=app_password
POSTGRES_HOST=db POSTGRES_HOST=db
POSTGRES_PORT=5432 POSTGRES_PORT=5432
# CORS / CSRF # CORS / CSRF
CORS_ALLOWED_ORIGINS=https://app.example.com CORS_ALLOWED_ORIGINS=https://qlockify.ir,https://www.qlockify.ir
CSRF_TRUSTED_ORIGINS=https://app.example.com CSRF_TRUSTED_ORIGINS=https://api.qlockify.ir,https://qlockify.ir,https://www.qlockify.ir
# JWT # JWT
ACCESS_TOKEN_LIFETIME=5 ACCESS_TOKEN_LIFETIME=5
@@ -40,5 +40,5 @@ CELERY_RESULT_BACKEND=redis://redis:6379/0
LANGUAGE_CODE=en-us LANGUAGE_CODE=en-us
TIME_ZONE=Asia/Tehran TIME_ZONE=Asia/Tehran
SMS_APIKEY= SMS_APIKEY=
BASE_URL= BASE_URL=https://api.qlockify.ir

View File

@@ -1,25 +1,34 @@
FROM python:3.14-slim FROM python:3.14
WORKDIR /app WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENV PIP_INDEX_URL=https://package-mirror.liara.ir/repository/pypi/simple
# Adapted Runflare mirror for Debian-based official Python image # ---- APT Iran-safe configuration ----
RUN . /etc/os-release && \ RUN rm -f /etc/apt/sources.list.d/debian.sources && \
echo "deb http://mirror-linux.runflare.com/debian $VERSION_CODENAME main" > /etc/apt/sources.list && \ printf '%s\n' \
echo "deb http://mirror-linux.runflare.com/debian $VERSION_CODENAME-updates main" >> /etc/apt/sources.list && \ 'deb http://mirror.arvancloud.ir/debian trixie main contrib non-free non-free-firmware' \
echo "deb http://mirror-linux.runflare.com/debian-security $VERSION_CODENAME-security main" >> /etc/apt/sources.list 'deb http://mirror.arvancloud.ir/debian trixie-updates main contrib non-free non-free-firmware' \
'deb http://mirror.arvancloud.ir/debian-security trixie-security main contrib non-free non-free-firmware' \
> /etc/apt/sources.list && \
echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf.d/99no-check-valid
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y gcc libpq-dev \ && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
rustc \
cargo \
pkg-config \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY qlockify-backend/requirements/ /app/requirements/ COPY requirements/ /app/requirements/
RUN pip install --no-cache-dir -r requirements/base.txt \
&& pip install --no-cache-dir -r requirements/prod.txt RUN pip install --no-cache-dir --upgrade pip setuptools wheel \
&& pip install --no-cache-dir -r requirements/base.txt \
COPY qlockify-backend/ . && pip install --no-cache-dir -r requirements/prod.txt
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"] COPY . .
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]

View File

@@ -1,15 +1,13 @@
services: services:
db: db:
image: postgres:18-alpine image: postgres:18-alpine
restart: always restart: always
env_file: env_file:
- .env - ./backend/qlockify-backend-deployment/.env
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql
- ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
- ./postgres/custom-postgresql.conf:/etc/postgresql/postgresql.conf:ro command: postgres
- ./postgres/pg_hba.conf:/var/lib/postgresql/data/pg_hba.conf:ro
command: postgres -c config_file=/etc/postgresql/postgresql.conf
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s interval: 10s
@@ -25,21 +23,27 @@ services:
- "127.0.0.1:6379:6379" - "127.0.0.1:6379:6379"
backend: backend:
build: build:
context: .. context: ./backend/qlockify-backend-deployment
dockerfile: qlockify-deployment/backend/Dockerfile dockerfile: ../Dockerfile
restart: always restart: always
env_file: env_file:
- ./backend/.env - ./backend/qlockify-backend-deployment/.env
volumes: volumes:
- static_data:/app/staticfiles - static_data:/app/static
- media_data:/app/mediafiles - media_data:/app/media
command: > command: >
sh -c "python manage.py migrate && sh -c "python manage.py migrate &&
python manage.py collectstatic --noinput && python manage.py collectstatic --noinput &&
gunicorn config.wsgi:application --bind 0.0.0.0:8000" gunicorn config.wsgi:application
expose: --bind 0.0.0.0:8000
- "8000" --worker-class gthread
--workers 2
--threads 8
--timeout 120
--keep-alive 75"
expose:
- "8000"
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
@@ -48,43 +52,62 @@ services:
celery: celery:
build: build:
context: .. context: ./backend/qlockify-backend-deployment
dockerfile: qlockify-deployment/backend/Dockerfile dockerfile: ../Dockerfile
restart: always restart: always
env_file: env_file:
- ./backend/.env - ./backend/qlockify-backend-deployment/.env
volumes: volumes:
- media_data:/app/mediafiles - media_data:/app/media
command: celery -A config worker -l INFO command: celery -A config worker -l INFO
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
redis: redis:
condition: service_started condition: service_started
backend: backend:
condition: service_started condition: service_started
celery-beat:
build:
context: ./backend/qlockify-backend-deployment
dockerfile: ../Dockerfile
restart: always
env_file:
- ./backend/qlockify-backend-deployment/.env
volumes:
- celery_beat_data:/app/run
command: celery -A config beat -l INFO --schedule /app/run/celerybeat-schedule
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
backend:
condition: service_started
frontend: frontend:
build: build:
context: .. context: ./frontend/qlockify-frontend-deployment
dockerfile: qlockify-deployment/frontend/Dockerfile dockerfile: ../Dockerfile
restart: always restart: always
env_file: env_file:
- ./frontend/.env - ./frontend/qlockify-frontend-deployment/.env
expose: expose:
- "80" - "80"
nginx: nginx:
image: nginx:alpine image: nginx:alpine
restart: always restart: always
ports: ports:
- "80:80" - "80:80"
# - "443:443" # Uncomment when adding SSL - "443:443"
volumes: volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/.htpasswd:/etc/nginx/.htpasswd:ro - ./nginx/certs:/etc/nginx/certs:ro
- static_data:/usr/share/nginx/html/staticfiles:ro - ./nginx/.htpasswd:/etc/nginx/.htpasswd:ro
- media_data:/usr/share/nginx/html/mediafiles:ro - static_data:/usr/share/nginx/html/static:ro
- media_data:/usr/share/nginx/html/media:ro
depends_on: depends_on:
- backend - backend
- frontend - frontend
@@ -93,3 +116,4 @@ volumes:
postgres_data: postgres_data:
static_data: static_data:
media_data: media_data:
celery_beat_data:

View File

@@ -1 +1 @@
VITE_API_BASE_URL=http://localhost/api VITE_API_BASE_URL=https://api.qlockify.ir

View File

@@ -1,18 +1,35 @@
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
RUN npm config set registry https://package-mirror.liara.ir/repository/npm/ --global RUN npm config set registry https://package-mirror.liara.ir/repository/npm/ --global
COPY qlockify-frontend/package*.json ./ COPY package*.json ./
RUN npm install RUN npm install
COPY qlockify-frontend/ . COPY . .
RUN npm run build RUN npm run build
FROM nginx:alpine FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html COPY --from=builder /app/dist /usr/share/nginx/html
# Internal Nginx configuration (Root Nginx acts as reverse proxy to this)
EXPOSE 80 RUN cat <<'EOF' > /etc/nginx/conf.d/default.conf
CMD ["nginx", "-g", "daemon off;"] server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location /assets/ {
try_files $uri =404;
access_log off;
expires 30d;
}
location / {
try_files $uri $uri/ /index.html;
}
}
EOF
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1 +1 @@
admin:$2y$05$gSZ4s3BN8TsEc.pS/vaZi.v/AMrIozncWtFDGkNOglJlv59f7jc7i Admin:$2y$10$YQpMsUAwPos59XKbWQRiSOxBJI4WmMbmTJBVy2ZRz/42FG.dj4W8K

1
nginx/certs/.gitkeep Normal file
View File

@@ -0,0 +1 @@

View File

@@ -1,55 +1,150 @@
server { server {
listen 80; listen 80;
server_name localhost; server_name qlockify.ir www.qlockify.ir;
client_max_body_size 100M; return 301 https://qlockify.ir$request_uri;
sendfile on; }
# Static and Media files server {
location /static/ { listen 80;
alias /usr/share/nginx/html/staticfiles/; server_name api.qlockify.ir;
expires 30d;
access_log off; return 301 https://api.qlockify.ir$request_uri;
} }
location /media/ { server {
alias /usr/share/nginx/html/mediafiles/; listen 443 ssl;
expires 30d; http2 on;
access_log off; server_name www.qlockify.ir;
}
ssl_certificate /etc/nginx/certs/fullchain.pem;
# Protect API Documentation with Basic Auth (from your old project) ssl_certificate_key /etc/nginx/certs/privkey.pem;
location ~ ^/(docs|redoc|openapi.json|api/docs|api/redoc|api/openapi.json|api/v1/docs) { ssl_protocols TLSv1.2 TLSv1.3;
auth_basic "Restricted API Documentation"; ssl_prefer_server_ciphers on;
auth_basic_user_file /etc/nginx/.htpasswd; ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
proxy_pass http://backend:8000;
proxy_set_header Host $host; return 301 https://qlockify.ir$request_uri;
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; server {
} listen 443 ssl;
http2 on;
# Standard API Proxy server_name qlockify.ir;
location /api/ {
proxy_pass http://backend:8000; ssl_certificate /etc/nginx/certs/fullchain.pem;
proxy_set_header Host $host; ssl_certificate_key /etc/nginx/certs/privkey.pem;
proxy_set_header X-Real-IP $remote_addr; ssl_protocols TLSv1.2 TLSv1.3;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; ssl_prefer_server_ciphers on;
} ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
# Admin Panel Proxy
location /admin/ { client_max_body_size 100M;
proxy_pass http://backend:8000; sendfile on;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; location /api/ {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; return 301 https://api.qlockify.ir$request_uri;
} }
# Frontend Proxy location /admin/ {
location / { return 301 https://api.qlockify.ir$request_uri;
proxy_pass http://frontend:80; }
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; location /docs {
} return 301 https://api.qlockify.ir$request_uri;
} }
location /redoc {
return 301 https://api.qlockify.ir$request_uri;
}
location /openapi.json {
return 301 https://api.qlockify.ir$request_uri;
}
location /static/ {
return 301 https://api.qlockify.ir$request_uri;
}
location /media/ {
return 301 https://api.qlockify.ir$request_uri;
}
location / {
proxy_pass http://frontend:80;
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;
}
}
server {
listen 443 ssl;
http2 on;
server_name api.qlockify.ir;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
client_max_body_size 100M;
sendfile on;
location /static/ {
alias /usr/share/nginx/html/static/;
expires 30d;
access_log off;
}
location /media/ {
alias /usr/share/nginx/html/media/;
expires 30d;
access_log off;
}
location ~ ^/(docs|redoc|openapi.json|api/docs|api/redoc|api/openapi.json|api/v1/docs) {
auth_basic "Restricted API Documentation";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://backend:8000;
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;
}
location /api/notifications/stream/ {
proxy_pass http://backend:8000;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_set_header Connection "";
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;
add_header X-Accel-Buffering no;
}
location /api/ {
proxy_pass http://backend:8000;
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;
}
location /admin/ {
proxy_pass http://backend:8000;
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;
}
}

View File

@@ -1,11 +0,0 @@
# TYPE DATABASE USER ADDRESS METHOD
local all all scram-sha-256
host all all 127.0.0.1/32 scram-sha-256
# Allow Docker containers to connect (Standard Docker bridge subnets)
host all all 172.16.0.0/12 scram-sha-256
host all all 192.168.0.0/16 scram-sha-256
# Reject everything else
host all all 0.0.0.0/0 reject
host all all ::/0 reject