Compare commits

..

12 Commits

11 changed files with 525 additions and 168 deletions

5
.gitignore vendored
View File

@@ -1,3 +1,8 @@
.env
backend/logs/
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
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`
- `../qlockify-frontend`
## Related Repositories
## 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
Qlockify/
qlockify-backend/
qlockify-frontend/
qlockify-deployment/
qlockify-deployment/
backend/
Dockerfile
.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:
- `./.env`
- `./backend/.env`
- `./frontend/.env`
2. From `qlockify-deployment`, build and start the stack:
### 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 --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
DEBUG=True
# Django Core
DJANGO_SETTINGS_MODULE=config.settings
DJANGO_SECRET_KEY=
DJANGO_ALLOWED_HOSTS=
# Django Core
DJANGO_SETTINGS_MODULE=config.settings
DJANGO_SECRET_KEY=
DJANGO_ALLOWED_HOSTS=api.qlockify.ir,qlockify.ir,www.qlockify.ir
# Database
POSTGRES_DB=app_db
@@ -14,9 +14,9 @@ POSTGRES_PASSWORD=app_password
POSTGRES_HOST=db
POSTGRES_PORT=5432
# CORS / CSRF
CORS_ALLOWED_ORIGINS=https://app.example.com
CSRF_TRUSTED_ORIGINS=https://app.example.com
# CORS / CSRF
CORS_ALLOWED_ORIGINS=https://qlockify.ir,https://www.qlockify.ir
CSRF_TRUSTED_ORIGINS=https://api.qlockify.ir,https://qlockify.ir,https://www.qlockify.ir
# JWT
ACCESS_TOKEN_LIFETIME=5
@@ -40,5 +40,5 @@ CELERY_RESULT_BACKEND=redis://redis:6379/0
LANGUAGE_CODE=en-us
TIME_ZONE=Asia/Tehran
SMS_APIKEY=
BASE_URL=
SMS_APIKEY=
BASE_URL=https://api.qlockify.ir

View File

@@ -1,25 +1,34 @@
FROM python:3.14-slim
FROM python:3.14
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=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
RUN . /etc/os-release && \
echo "deb http://mirror-linux.runflare.com/debian $VERSION_CODENAME main" > /etc/apt/sources.list && \
echo "deb http://mirror-linux.runflare.com/debian $VERSION_CODENAME-updates main" >> /etc/apt/sources.list && \
echo "deb http://mirror-linux.runflare.com/debian-security $VERSION_CODENAME-security main" >> /etc/apt/sources.list
# ---- APT Iran-safe configuration ----
RUN rm -f /etc/apt/sources.list.d/debian.sources && \
printf '%s\n' \
'deb http://mirror.arvancloud.ir/debian trixie main contrib non-free non-free-firmware' \
'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 \
&& 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/*
COPY qlockify-backend/requirements/ /app/requirements/
RUN pip install --no-cache-dir -r requirements/base.txt \
&& pip install --no-cache-dir -r requirements/prod.txt
COPY qlockify-backend/ .
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
COPY requirements/ /app/requirements/
RUN pip install --no-cache-dir --upgrade pip setuptools wheel \
&& pip install --no-cache-dir -r requirements/base.txt \
&& pip install --no-cache-dir -r requirements/prod.txt
COPY . .
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]

View File

@@ -1,15 +1,13 @@
services:
db:
services:
db:
image: postgres:18-alpine
restart: always
env_file:
- .env
- ./backend/qlockify-backend-deployment/.env
volumes:
- postgres_data:/var/lib/postgresql/data
- postgres_data:/var/lib/postgresql
- ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
- ./postgres/custom-postgresql.conf:/etc/postgresql/postgresql.conf:ro
- ./postgres/pg_hba.conf:/var/lib/postgresql/data/pg_hba.conf:ro
command: postgres -c config_file=/etc/postgresql/postgresql.conf
command: postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
@@ -25,21 +23,27 @@ services:
- "127.0.0.1:6379:6379"
backend:
build:
context: ..
dockerfile: qlockify-deployment/backend/Dockerfile
build:
context: ./backend/qlockify-backend-deployment
dockerfile: ../Dockerfile
restart: always
env_file:
- ./backend/.env
env_file:
- ./backend/qlockify-backend-deployment/.env
volumes:
- static_data:/app/staticfiles
- media_data:/app/mediafiles
- static_data:/app/static
- media_data:/app/media
command: >
sh -c "python manage.py migrate &&
python manage.py collectstatic --noinput &&
gunicorn config.wsgi:application --bind 0.0.0.0:8000"
expose:
- "8000"
gunicorn config.wsgi:application
--bind 0.0.0.0:8000
--worker-class gthread
--workers 2
--threads 8
--timeout 120
--keep-alive 75"
expose:
- "8000"
depends_on:
db:
condition: service_healthy
@@ -48,43 +52,62 @@ services:
celery:
build:
context: ..
dockerfile: qlockify-deployment/backend/Dockerfile
context: ./backend/qlockify-backend-deployment
dockerfile: ../Dockerfile
restart: always
env_file:
- ./backend/.env
volumes:
- media_data:/app/mediafiles
command: celery -A config worker -l INFO
env_file:
- ./backend/qlockify-backend-deployment/.env
volumes:
- media_data:/app/media
command: celery -A config worker -l INFO
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
backend:
condition: service_started
redis:
condition: service_started
backend:
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:
build:
context: ..
dockerfile: qlockify-deployment/frontend/Dockerfile
context: ./frontend/qlockify-frontend-deployment
dockerfile: ../Dockerfile
restart: always
env_file:
- ./frontend/.env
- ./frontend/qlockify-frontend-deployment/.env
expose:
- "80"
nginx:
image: nginx:alpine
restart: always
ports:
- "80:80"
# - "443:443" # Uncomment when adding SSL
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/.htpasswd:/etc/nginx/.htpasswd:ro
- static_data:/usr/share/nginx/html/staticfiles:ro
- media_data:/usr/share/nginx/html/mediafiles:ro
nginx:
image: nginx:alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/certs:/etc/nginx/certs:ro
- ./nginx/.htpasswd:/etc/nginx/.htpasswd:ro
- static_data:/usr/share/nginx/html/static:ro
- media_data:/usr/share/nginx/html/media:ro
depends_on:
- backend
- frontend
@@ -93,3 +116,4 @@ volumes:
postgres_data:
static_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
WORKDIR /app
RUN npm config set registry https://package-mirror.liara.ir/repository/npm/ --global
COPY qlockify-frontend/package*.json ./
FROM node:20-alpine AS builder
WORKDIR /app
RUN npm config set registry https://package-mirror.liara.ir/repository/npm/ --global
COPY package*.json ./
RUN npm install
COPY qlockify-frontend/ .
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
# Internal Nginx configuration (Root Nginx acts as reverse proxy to this)
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
RUN cat <<'EOF' > /etc/nginx/conf.d/default.conf
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 {
listen 80;
server_name localhost;
client_max_body_size 100M;
sendfile on;
# Static and Media files
location /static/ {
alias /usr/share/nginx/html/staticfiles/;
expires 30d;
access_log off;
}
location /media/ {
alias /usr/share/nginx/html/mediafiles/;
expires 30d;
access_log off;
}
# Protect API Documentation with Basic Auth (from your old project)
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;
}
# Standard API Proxy
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;
}
# Admin Panel Proxy
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;
}
# Frontend Proxy
location / {
proxy_pass http://frontend:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
server {
listen 80;
server_name qlockify.ir www.qlockify.ir;
return 301 https://qlockify.ir$request_uri;
}
server {
listen 80;
server_name api.qlockify.ir;
return 301 https://api.qlockify.ir$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name www.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;
return 301 https://qlockify.ir$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name 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 /api/ {
return 301 https://api.qlockify.ir$request_uri;
}
location /admin/ {
return 301 https://api.qlockify.ir$request_uri;
}
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