commit b4c6b3c0126f3a3df9b9474719df4e6bc7bdad3a Author: Amirhossein Khalili Date: Tue May 19 20:57:09 2026 +0330 initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ea3f148 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +NEXT_HOST=east-guilan-ce.ir +LETSENCRYPT_EMAIL=admin@east-guilan-ce.ir diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml new file mode 100644 index 0000000..11e931f --- /dev/null +++ b/.github/workflows/deployment.yml @@ -0,0 +1,89 @@ +name: Deployment CI/CD + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Prepare local validation layout + run: | + cp .env.example .env + mkdir -p backend/guilan-ace-backend frontend/guilan-ace-frontend + cat <<'EOF' > backend/guilan-ace-backend/.env + DJANGO_SETTINGS_MODULE=config.settings.production + SECRET_KEY=validate + DEBUG=False + ALLOWED_HOSTS=api.example.com + DJANGO_HOST=https://api.example.com + DB_ENGINE=django.db.backends.postgresql + DB_NAME=app + DB_USER=app + DB_PASSWORD=password + DB_HOST=db + DB_PORT=5432 + REDIS_PASSWORD=password + REDIS_URL=redis://:password@redis:6379/0 + CELERY_BROKER_URL=redis://:password@redis:6379/0 + CELERY_RESULT_BACKEND=redis://:password@redis:6379/1 + EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend + EMAIL_HOST=localhost + EMAIL_PORT=587 + EMAIL_USE_TLS=False + EMAIL_HOST_USER= + EMAIL_HOST_PASSWORD= + DEFAULT_FROM_EMAIL=noreply@example.com + JWT_SECRET_KEY=validate + JWT_ALGORITHM=HS256 + JWT_ACCESS_TOKEN_LIFETIME=3600 + JWT_REFRESH_TOKEN_LIFETIME=86400 + CORS_ALLOWED_ORIGINS=https://frontend.example.com + FRONTEND_ROOT=https://frontend.example.com + FRONTEND_PASSWORD_RESET_PAGE=https://frontend.example.com/reset-password + FRONTEND_CALLBACK_URL=https://frontend.example.com/payments/result + ZARINPAL_MERCHANT_ID=test + ZARINPAL_USE_SANDBOX=True + ZARINPAL_CALLBACK_URL=https://api.example.com/api/payments/callback + GUNICORN_WORKERS=2 + GUNICORN_THREADS=2 + GUNICORN_TIMEOUT=120 + EOF + cat <<'EOF' > frontend/guilan-ace-frontend/.env + VITE_API_BASE_URL=https://api.example.com + EOF + + - name: Validate compose config + run: docker compose config + + deploy: + runs-on: ubuntu-latest + needs: validate + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + timeout-minutes: 30 + steps: + - name: Deploy compose stack + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + port: ${{ secrets.DEPLOY_PORT }} + script: | + set -e + cd "${{ secrets.DEPLOY_PATH }}" + git fetch --prune origin + git checkout "${{ vars.DEPLOY_BRANCH || 'main' }}" + git pull --ff-only origin "${{ vars.DEPLOY_BRANCH || 'main' }}" + docker compose up -d --build --remove-orphans + docker image prune -f diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44b143b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +data/ +certs/fullchain.pem +certs/privateKey.pem +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..952caed --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# Guilan ACE Deployment + +## Local and deployment layout +```text +parent-directory/ + guilan-ace-backend/ + guilan-ace-frontend/ + guilan-ace-deployment/ + backend/ + Dockerfile + frontend/ + Dockerfile + certs/ + traefik/ + docker-compose.yml + grafana-datasources.yml + nginx-static.conf + prometheus.yml +``` + +## Usage +1. Keep `guilan-ace-backend`, `guilan-ace-frontend`, and `guilan-ace-deployment` as sibling repositories in your local workspace. +2. Copy `.env.example` to `.env`. +3. On the deployment server, place the backend repo at `guilan-ace-deployment/backend/guilan-ace-backend`. +4. On the deployment server, place the frontend repo at `guilan-ace-deployment/frontend/guilan-ace-frontend`. +5. Create `backend/guilan-ace-backend/.env` from the backend repo sample. +6. Run `docker compose up -d --build`. + +## Notes +- Traefik terminates TLS and routes frontend, API, admin, static, media, Grafana, Prometheus, and Uptime Kuma. +- Alertmanager has been removed from this stack. Prometheus scraping and Grafana provisioning remain intact. +- Dockerfiles live only in this deployment repository, and compose is intentionally wired only for the nested production layout. diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..a6031f8 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,42 @@ +FROM python:3.13-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Set work directory +WORKDIR /app + +RUN rm -f /etc/apt/sources.list.d/debian.sources && \ + printf '%s\n' \ + 'deb http://mirror-linux.runflare.com/debian trixie main' \ + 'deb http://mirror-linux.runflare.com/debian-security trixie-security main' \ + > /etc/apt/sources.list && \ + echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf.d/99no-check-valid + + +# Install system dependencies +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + postgresql-client \ + build-essential \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt /app/ +RUN pip install --no-cache-dir -r requirements.txt -i https://package-mirror.liara.ir/repository/pypi/simple + +# Copy project +COPY . /app/ + +# Create directories for static and media files +RUN mkdir -p /app/static /app/media +# COPY ./static/ /app/static/ + +# Collect static files +RUN python manage.py collectstatic --noinput || true + +EXPOSE 8000 + +CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers=3", "--threads=2", "--timeout=60"] diff --git a/certs/fullchain.example.pem b/certs/fullchain.example.pem new file mode 100644 index 0000000..90fa9fb --- /dev/null +++ b/certs/fullchain.example.pem @@ -0,0 +1,64 @@ +-----BEGIN CERTIFICATE----- +MIIGEzCCBPugAwIBAgISBZf+U3m3Aftq6nyZTh1KZD6tMA0GCSqGSIb3DQEBCwUA +MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD +EwNSMTIwHhcNMjYwNTE4MTQyOTI0WhcNMjYwODE2MTQyOTIzWjAeMRwwGgYDVQQD +DBMqLmVhc3QtZ3VpbGFuLWNlLmlyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAx0eyNPO1YVzOZGiC19l5IEedFeMe1Yf2T1srJUr9MSpDKKiE8n3AY0Jq +9WgUou9E8ZViu3cYjE8UEuP6s4W+U1iXYWMwqc6hAkSejn8mb4vSdIO1dVEW9BNM +spDgXZvbwSs6UWm+sUpxwot4hV9RlzIdSoZ4nrLRJnu7OSW/fO7xU8UNPAgPHarH +e/xBPPTeYKq+CcDb7HJUdJMYxDd1oRtZQm/Uz5rDrqf0R4DxAhUBcZXgp8zn4yH1 +nPjIR+2XUCB2n4QlOfNqhiPa9JwD6ZVrUImaFBdDTZjenE/HHVJ94k1LzMTGgHRv +Cp/avx5fw4lYlY8J72eKKFK77fHQpUUk7F7klZ+CHDlrLi+RDyqiJezhVJlPuXXn +ivEWvrrVN0EZ+1pO+Xn1xqBDjJ2KP+VbGzSveGgxsO5XHy2BaGumYx7nQwhqynXk +BMKC5PCufu8zfvsd2ODSGUvIhihgsXbFRaWEWOQiLqFq3z/4GORnn09DvBqnIsKQ +LGI4xDrnb/JYmkCs5zxLey1kDWOtqRjzZ/aVzSUM8h1UOmofXKPGg/ucM1SMEqe5 +qLyifgYsxZ/OUnrx6OZt1xW4eROGxovyehscaCIIMeIAhKZ4oiJhWfZ4mgb9teiF +RylJST+YXqN9/DydVe6AyIUInKAQZL5PsY36B/QK6Fckb2GnOK8CAwEAAaOCAjQw +ggIwMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMB +Af8EAjAAMB0GA1UdDgQWBBTU6TZIKc65xcaDLlCRk8padrq7yjAfBgNVHSMEGDAW +gBQAtSnyLY5vMeibTK14Pvrc6QzR0jAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUH +MAKGF2h0dHA6Ly9yMTIuaS5sZW5jci5vcmcvMDEGA1UdEQQqMCiCEyouZWFzdC1n +dWlsYW4tY2UuaXKCEWVhc3QtZ3VpbGFuLWNlLmlyMBMGA1UdIAQMMAowCAYGZ4EM +AQIBMC4GA1UdHwQnMCUwI6AhoB+GHWh0dHA6Ly9yMTIuYy5sZW5jci5vcmcvNDYu +Y3JsMIIBDAYKKwYBBAHWeQIEAgSB/QSB+gD4AHcAwjF+V0UZo0XufzjespBB68fC +IVoiv3/Vta12mtkOUs0AAAGeO7NpVgAABAMASDBGAiEAmxz+oc1QeAR1J/yEe1jZ +W2hT/U3XF+5q63O+kRjQWO0CIQCGI+xwY/hhPjJr9HkPRTI5NXGt9EeVe0vMAu1s ++TMtxgB9ABqLnWsP/r+BtHk5xtIxCobW0QLU8EbiGCyd419eJiXvAAABnjuza0wA +CAAABQAUIto1BAMARjBEAiAPAyeQN4zkD/vUAAqo/8sF5uKicaul9fS9y0bUv+8d +hgIgQxClI5FycnYL6GTwBxpNWS0uWbDhoTAtj+Mw6NOGOD8wDQYJKoZIhvcNAQEL +BQADggEBAHt+R9Da16AbLPZqC5BelK6prKdmeaqkDIDO6aE1aZyuS0xxK228fPAr +zcopyWI4Onm29bAYxaeFtUwZurDyqb+jHf0AD8PC2zOFxQCvDyEO9l28yQ51hSR2 +6KlTNfsUaBAdZctSseZI1wcqJ3cwZLGAJjrTXaMDDDc83UeG/bJ8syU+iGlUhhWu +e/hEFTtIQ14U6lvNLwgtsC3Ptwe5gGIauJ+wtTIkavulGrWpGLgEUQPqNFz/NmwM ++Nis+oB2qKMJfQBoQgNIVFahsnOvUTJZbx3nEKQfI/OAGQHH2fr6xpg09zbi0j/5 +Y8q58R3hzVyBdJHytdcI5W/pEJmYzSE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFBjCCAu6gAwIBAgIRAMISMktwqbSRcdxA9+KFJjwwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw +WhcNMjcwMzEyMjM1OTU5WjAzMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDEMMAoGA1UEAxMDUjEyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2pgodK2+lP474B7i5Ut1qywSf+2nAzJ+Npfs6DGPpRONC5kuHs0BUT1M +5ShuCVUxqqUiXXL0LQfCTUA83wEjuXg39RplMjTmhnGdBO+ECFu9AhqZ66YBAJpz +kG2Pogeg0JfT2kVhgTU9FPnEwF9q3AuWGrCf4yrqvSrWmMebcas7dA8827JgvlpL +Thjp2ypzXIlhZZ7+7Tymy05v5J75AEaz/xlNKmOzjmbGGIVwx1Blbzt05UiDDwhY +XS0jnV6j/ujbAKHS9OMZTfLuevYnnuXNnC2i8n+cF63vEzc50bTILEHWhsDp7CH4 +WRt/uTp8n1wBnWIEwii9Cq08yhDsGwIDAQABo4H4MIH1MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwEgYDVR0TAQH/BAgwBgEB +/wIBADAdBgNVHQ4EFgQUALUp8i2ObzHom0yteD763OkM0dIwHwYDVR0jBBgwFoAU +ebRZ5nu25eQBc4AIiMgaWPbpm24wMgYIKwYBBQUHAQEEJjAkMCIGCCsGAQUFBzAC +hhZodHRwOi8veDEuaS5sZW5jci5vcmcvMBMGA1UdIAQMMAowCAYGZ4EMAQIBMCcG +A1UdHwQgMB4wHKAaoBiGFmh0dHA6Ly94MS5jLmxlbmNyLm9yZy8wDQYJKoZIhvcN +AQELBQADggIBAI910AnPanZIZTKS3rVEyIV29BWEjAK/duuz8eL5boSoVpHhkkv3 +4eoAeEiPdZLj5EZ7G2ArIK+gzhTlRQ1q4FKGpPPaFBSpqV/xbUb5UlAXQOnkHn3m +FVj+qYv87/WeY+Bm4sN3Ox8BhyaU7UAQ3LeZ7N1X01xxQe4wIAAE3JVLUCiHmZL+ +qoCUtgYIFPgcg350QMUIWgxPXNGEncT921ne7nluI02V8pLUmClqXOsCwULw+PVO +ZCB7qOMxxMBoCUeL2Ll4oMpOSr5pJCpLN3tRA2s6P1KLs9TSrVhOk+7LX28NMUlI +usQ/nxLJID0RhAeFtPjyOCOscQBA53+NRjSCak7P4A5jX7ppmkcJECL+S0i3kXVU +y5Me5BbrU8973jZNv/ax6+ZK6TM8jWmimL6of6OrX7ZU6E2WqazzsFrLG3o2kySb +zlhSgJ81Cl4tv3SbYiYXnJExKQvzf83DYotox3f0fwv7xln1A2ZLplCb0O+l/AK0 +YE0DS2FPxSAHi0iwMfW2nNHJrXcY3LLHD77gRgje4Eveubi2xxa+Nmk/hmhLdIET +iVDFanoCrMVIpQ59XWHkzdFmoHXHBV7oibVjGSO7ULSQ7MJ1Nz51phuDJSgAIU7A +0zrLnOrAj/dfrlEWRhCvAgbuwLZX1A2sjNjXoPOHbsPiy+lO1KF8/XY7 +-----END CERTIFICATE----- diff --git a/certs/privateKey.examole.pem b/certs/privateKey.examole.pem new file mode 100644 index 0000000..326a49c --- /dev/null +++ b/certs/privateKey.examole.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +Fsu0vT7dhy5GJP6Qmrt7ogjJ9K4grQ== +-----END PRIVATE KEY----- diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..253c6ff --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,233 @@ +name: east-guilan + +services: + traefik: + image: traefik:v2.11 + entrypoint: ["/bin/sh", "/traefik-entrypoint.sh"] + command: + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --entrypoints.web.http.redirections.entryPoint.to=websecure + - --entrypoints.web.http.redirections.entryPoint.scheme=https + - --certificatesresolvers.le.acme.email=${LETSENCRYPT_EMAIL} + - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json + - --certificatesresolvers.le.acme.httpchallenge=true + - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web + - --metrics.prometheus=true + - --metrics.prometheus.addEntryPointsLabels=true + - --metrics.prometheus.addRoutersLabels=true + - --metrics.prometheus.addServicesLabels=true + - --entrypoints.metrics.address=:8082 + - --metrics.prometheus.entryPoint=metrics + - --api.dashboard=true + - traefik.http.routers.metrics.rule=Host(`api.east-guilan-ce.ir`) && Path(`/metrics`) + - traefik.http.routers.metrics.entrypoints=websecure + - traefik.http.routers.metrics.tls.certresolver=le + - traefik.http.services.metrics.loadbalancer.server.port=8000 + ports: + - "80:80" + - "443:443" + volumes: + - ./traefik/entrypoint.sh:/traefik-entrypoint.sh:ro + - ./certs:/certs:ro + - traefik_letsencrypt:/letsencrypt + - /var/run/docker.sock:/var/run/docker.sock:ro + + db: + image: postgres:16-alpine + env_file: + - ./backend/guilan-ace-backend/.env + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${DB_USER} -d $${DB_NAME} -h 127.0.0.1"] + interval: 10s + timeout: 5s + retries: 10 + volumes: + - postgres_data:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + env_file: + - ./backend/guilan-ace-backend/.env + command: + - /bin/sh + - -c + - redis-server --appendonly yes --requirepass "$${REDIS_PASSWORD}" + healthcheck: + test: ["CMD-SHELL", "redis-cli -a \"$${REDIS_PASSWORD}\" PING"] + interval: 10s + timeout: 5s + retries: 10 + volumes: + - redis_data:/data + + web: + build: + context: ./backend/guilan-ace-backend + dockerfile: ./backend/Dockerfile + env_file: + - ./backend/guilan-ace-backend/.env + environment: + PROMETHEUS_MULTIPROC_DIR: /tmp/prometheus + tmpfs: + - /tmp/prometheus + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - django_static:/app/staticfiles + - django_media:/app/media + command: > + sh -c "gunicorn config.wsgi:application + --bind 0.0.0.0:8000 + --workers=$${GUNICORN_WORKERS:-3} + --threads=$${GUNICORN_THREADS:-2} + --timeout=$${GUNICORN_TIMEOUT:-120}" + labels: + - traefik.enable=true + - traefik.http.routers.api.rule=Host(`api.east-guilan-ce.ir`) && PathPrefix(`/api`) + - traefik.http.routers.api.entrypoints=websecure + - traefik.http.routers.api.tls.certresolver=le + - traefik.http.routers.api.priority=10 + - traefik.http.services.api.loadbalancer.server.port=8000 + - traefik.http.routers.admin.rule=Host(`api.east-guilan-ce.ir`) && PathPrefix(`/admin`) + - traefik.http.routers.admin.entrypoints=websecure + - traefik.http.routers.admin.tls.certresolver=le + - traefik.http.routers.admin.priority=10 + + worker: + build: + context: ./backend/guilan-ace-backend + dockerfile: ./backend/Dockerfile + env_file: + - ./backend/guilan-ace-backend/.env + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + command: celery -A config.services.celery worker --loglevel=INFO + volumes: + - django_media:/app/media + + beat: + build: + context: ./backend/guilan-ace-backend + dockerfile: ./backend/Dockerfile + env_file: + - ./backend/guilan-ace-backend/.env + depends_on: + - worker + command: celery -A config.services.celery beat --loglevel=INFO + volumes: + - django_media:/app/media + + frontend: + build: + context: ./frontend/guilan-ace-frontend + dockerfile: ./frontend/Dockerfile + labels: + - traefik.enable=true + - traefik.http.routers.frontend.rule=Host(`${NEXT_HOST}`) + - traefik.http.routers.frontend.entrypoints=websecure + - traefik.http.routers.frontend.tls.certresolver=le + - traefik.http.services.frontend.loadbalancer.server.port=80 + + static: + image: nginx:1.27-alpine + volumes: + - ./nginx-static.conf:/etc/nginx/conf.d/default.conf:ro + - django_static:/var/www/static:ro + - django_media:/var/www/media:ro + labels: + - traefik.enable=true + - traefik.http.routers.static.rule=Host(`api.east-guilan-ce.ir`) && PathPrefix(`/static`) + - traefik.http.routers.static.entrypoints=websecure + - traefik.http.routers.static.tls.certresolver=le + - traefik.http.routers.static.priority=20 + - traefik.http.services.static.loadbalancer.server.port=80 + - traefik.http.routers.media.rule=Host(`api.east-guilan-ce.ir`) && PathPrefix(`/media`) + - traefik.http.routers.media.entrypoints=websecure + - traefik.http.routers.media.tls.certresolver=le + - traefik.http.routers.media.priority=20 + + uptime: + image: louislam/uptime-kuma:1 + restart: unless-stopped + volumes: + - ./data/uptime:/app/data + labels: + - traefik.enable=true + - traefik.http.routers.kuma.rule=Host(`uptime.east-guilan-ce.ir`) + - traefik.http.routers.kuma.entrypoints=websecure + - traefik.http.routers.kuma.tls.certresolver=le + - traefik.http.services.kuma.loadbalancer.server.port=3001 + + node_exporter: + image: prom/node-exporter:v1.9.1 + restart: unless-stopped + ports: + - "9100:9100" + volumes: + - /:/host:ro,rslave + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /run/udev:/host/run/udev:ro + command: + - --path.rootfs=/host + - --path.procfs=/host/proc + - --path.sysfs=/host/sys + - --path.udev.data=/host/run/udev/data + + redis_exporter: + image: oliver006/redis_exporter:v1.62.0 + restart: unless-stopped + env_file: + - ./backend/guilan-ace-backend/.env + command: + - /bin/sh + - -c + - redis_exporter --redis.addr=redis://redis:6379 --redis.password="$${REDIS_PASSWORD}" + + grafana: + image: grafana/grafana + restart: unless-stopped + labels: + - traefik.enable=true + - traefik.http.routers.grafana.rule=Host(`grafana.east-guilan-ce.ir`) + - traefik.http.routers.grafana.entrypoints=websecure + - traefik.http.routers.grafana.tls.certresolver=le + - traefik.http.services.grafana.loadbalancer.server.port=3000 + volumes: + - grafana_data:/var/lib/grafana + - ./grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasource.yml:ro + environment: + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD: changeMeNow + GF_USERS_ALLOW_SIGN_UP: "false" + + prometheus: + image: prom/prometheus + volumes: + - prometheus_data:/prometheus + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + restart: unless-stopped + labels: + - traefik.enable=true + - traefik.http.routers.prom.rule=Host(`prometheus.east-guilan-ce.ir`) + - traefik.http.routers.prom.entrypoints=websecure + - traefik.http.routers.prom.tls.certresolver=le + - traefik.http.services.prom.loadbalancer.server.port=9090 + +volumes: + traefik_letsencrypt: + postgres_data: + redis_data: + django_media: + django_static: + prometheus_data: + grafana_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..7aa53d0 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,30 @@ +FROM node:20-alpine AS builder + +WORKDIR /app + +RUN npm config set registry https://package-mirror.liara.ir/repository/npm/ --global + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production image +FROM nginx:alpine + +# Copy built files to nginx +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/grafana-datasources.yml b/grafana-datasources.yml new file mode 100644 index 0000000..0eddf26 --- /dev/null +++ b/grafana-datasources.yml @@ -0,0 +1,7 @@ +apiVersion: 1 +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true diff --git a/nginx-static.conf b/nginx-static.conf new file mode 100644 index 0000000..5f7ddec --- /dev/null +++ b/nginx-static.conf @@ -0,0 +1,26 @@ +server { + listen 80; + server_name _; + + # Django collected static files + location /static/ { + alias /var/www/static/; + access_log off; + expires 30d; + add_header Cache-Control "public"; + } + + # Django media (user uploads) + location /media/ { + alias /var/www/media/; + access_log off; + expires 7d; + add_header Cache-Control "public"; + } + + # No dotfiles + location ~ /\. { + deny all; + } +} + diff --git a/prometheus.yml b/prometheus.yml new file mode 100644 index 0000000..402072b --- /dev/null +++ b/prometheus.yml @@ -0,0 +1,26 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["prometheus:9090"] + + - job_name: "node" + static_configs: + - targets: ["node_exporter:9100"] + + - job_name: "traefik" + metrics_path: /metrics + static_configs: + - targets: ["traefik:8082"] + + - job_name: "redis" + static_configs: + - targets: ["redis_exporter:9121"] + + - job_name: "django" + metrics_path: /metrics + static_configs: + - targets: ["web:8000"] diff --git a/traefik/entrypoint.sh b/traefik/entrypoint.sh new file mode 100644 index 0000000..f34ee2c --- /dev/null +++ b/traefik/entrypoint.sh @@ -0,0 +1,41 @@ +#!/bin/sh +set -eu + +DYNAMIC_DIR="/etc/traefik/dynamic" +TLS_CONFIG="${DYNAMIC_DIR}/custom-tls.yml" +CERT_FILE="" +KEY_FILE="" + +for candidate in /certs/fullchain.pem /certs/fullchain.crt; do + if [ -s "$candidate" ]; then + CERT_FILE="$candidate" + break + fi +done + +for candidate in /certs/privateKey.pem /certs/privatekey.pem /certs/privkey.pem; do + if [ -s "$candidate" ]; then + KEY_FILE="$candidate" + break + fi +done + +mkdir -p "$DYNAMIC_DIR" +rm -f "$TLS_CONFIG" + +if [ -n "$CERT_FILE" ] && [ -n "$KEY_FILE" ]; then + cat >"$TLS_CONFIG" <