Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: '3.12'
cache: 'pip'

- name: Install dependencies
Expand Down
120 changes: 63 additions & 57 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,87 +1,93 @@
name: CD Pipeline
name: Deploy to Production

on:
push:
branches:
- main

env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ghcr.io/${{ github.repository }}
IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }}

jobs:
lint:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm ci --prefix frontend/

- name: Run JS linter
run: npm run lint --prefix frontend/

build-and-push:
needs: lint
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

concurrency:
group: deploy-prod
cancel-in-progress: false

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Log in to the Container registry
uses: docker/login-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push Frontend
- name: Build and push Docker images
run: |
docker build -t ${{ env.IMAGE_PREFIX }}/frontend:latest ./frontend
docker push ${{ env.IMAGE_PREFIX }}/frontend:latest

- name: Build and push Backend
run: |
docker build -t ${{ env.IMAGE_PREFIX }}/backend:latest ./backend
docker build \
-t ${{ env.IMAGE_PREFIX }}/backend:${{ github.sha }} \
-t ${{ env.IMAGE_PREFIX }}/backend:latest \
./backend
docker push ${{ env.IMAGE_PREFIX }}/backend:${{ github.sha }}
docker push ${{ env.IMAGE_PREFIX }}/backend:latest
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Build and push ML Worker
run: |
docker build -t ${{ env.IMAGE_PREFIX }}/ml_worker:latest ./ml_core
docker push ${{ env.IMAGE_PREFIX }}/ml_worker:latest
docker build \
-t ${{ env.IMAGE_PREFIX }}/frontend:${{ github.sha }} \
-t ${{ env.IMAGE_PREFIX }}/frontend:latest \
./frontend

docker push ${{ env.IMAGE_PREFIX }}/frontend:${{ github.sha }}
docker push ${{ env.IMAGE_PREFIX }}/frontend:latest

deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Deploy to VPS via SSH
uses: appleboy/ssh-action@v1.0.3
docker build \
-t ${{ env.IMAGE_PREFIX }}/ml_core:${{ github.sha }} \
-t ${{ env.IMAGE_PREFIX }}/ml_core:latest \
./ml_core

docker push ${{ env.IMAGE_PREFIX }}/ml_core:${{ github.sha }}
docker push ${{ env.IMAGE_PREFIX }}/ml_core:latest

- name: Deploy to VPS
uses: appleboy/ssh-action@v1.2.5
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /opt/handwriter

echo "POSTGRES_USER=${{ secrets.PROD_DB_USER }}" > .env.tmp
echo "POSTGRES_PASSWORD=${{ secrets.PROD_DB_PASSWORD }}" >> .env.tmp
echo "POSTGRES_DB=${{ secrets.PROD_DB_NAME }}" >> .env.tmp
echo "REDIS_URL=redis://redis:6379/0" >> .env.tmp
echo "JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}" >> .env.tmp
echo "MINIO_ROOT_PASSWORD=${{ secrets.MINIO_ROOT_PASSWORD }}" >> .env.tmp
echo "TELEGRAM_BOT_TOKEN=${{ secrets.TELEGRAM_BOT_TOKEN }}" >> .env.tmp
echo "TELEGRAM_BOT_USERNAME=${{ secrets.TELEGRAM_BOT_USERNAME }}" >> .env.tmp

chmod 600 .env.tmp
mv .env.tmp .env

git pull origin main

echo "DATABASE_URL=${{ secrets.PROD_DATABASE_URL }}" > .env
echo "REDIS_URL=${{ secrets.PROD_REDIS_URL }}" >> .env
echo "MINIO_ROOT_PASSWORD=${{ secrets.MINIO_ROOT_PASSWORD }}" >> .env
echo "POSTGRES_USER=${{ secrets.PROD_DB_USER }}" >> .env
echo "POSTGRES_PASSWORD=${{ secrets.PROD_DB_PASSWORD }}" >> .env
echo "TELEGRAM_BOT_TOKEN=${{ secrets.TELEGRAM_BOT_TOKEN }}" >> .env
echo "TELEGRAM_BOT_USERNAME=${{ secrets.TELEGRAM_BOT_USERNAME }}" >> .env
echo "JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}" >> .env
echo "POSTGRES_DB=app_db" >> .env
git fetch --all
git reset --hard origin/main

docker compose pull
docker compose up -d --remove-orphans
docker compose run --rm backend_api alembic upgrade head
docker compose up -d

echo "Очікуємо 10 секунд для ініціалізації..."
sleep 10

if ! curl --fail --max-time 5 https://handwritter.me/api/health; then
echo "❌ Сервер впав при запуску. Логи:"
docker compose logs --tail=100 backend_api
exit 1
fi
echo "✅ Деплой успішно завершено!"
11 changes: 8 additions & 3 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]


RUN groupadd -r app && useradd -r -g app -d /app -s /sbin/nologin -c "Docker image user" app

RUN chown -R app:app /app

USER app

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4", "--proxy-headers", "--forwarded-allow-ips", "*"]
40 changes: 32 additions & 8 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ services:
max-size: "10m"
max-file: "3"
depends_on:
- frontend
- backend_api
backend_api:
condition: service_healthy
frontend:
condition: service_started
networks:
- app_network

Expand All @@ -25,6 +27,7 @@ services:
volumes:
- /var/www/certbot:/var/www/certbot
- /etc/letsencrypt:/etc/letsencrypt
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done'"
frontend:
image: ghcr.io/mashta-lilia/handwriter/frontend:latest
restart: unless-stopped
Expand Down Expand Up @@ -72,6 +75,13 @@ services:
condition: service_started
networks:
- app_network

healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; import sys; resp = urllib.request.urlopen('http://localhost:8000/api/health', timeout=5); sys.exit(0 if resp.getcode() == 200 else 1)"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
Comment thread
coderabbitai[bot] marked this conversation as resolved.

ml_worker:
image: ghcr.io/mashta-lilia/handwriter/ml_worker:latest
Expand All @@ -98,9 +108,12 @@ services:
max-size: "10m"
max-file: "3"
depends_on:
- redis
- db
- minio
db:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_healthy
networks:
- app_network

Expand Down Expand Up @@ -148,6 +161,11 @@ services:
- redis_data:/data
networks:
- app_network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5

minio:
image: minio/minio
Expand All @@ -163,19 +181,25 @@ services:
- minio_data:/data
networks:
- app_network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 10s
retries: 3

minio-init:
image: minio/mc
environment:
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
depends_on:
- minio
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
sleep 5;
mc alias set myminio http://minio:9000 admin ${MINIO_ROOT_PASSWORD};
mc mb myminio/handwriting-assets || true;
mc mb myminio/handwriting-words || true;
mc anonymous set public myminio/handwriting-assets;
mc anonymous set public myminio/handwriting-words;
exit 0;
"

Expand Down
7 changes: 6 additions & 1 deletion infra/nginx/default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ server {
}

server {
listen 443 ssl;
listen 443 ssl http2;
server_name handwritter.me www.handwritter.me;

# Твои новые сертификаты
Expand All @@ -25,6 +25,11 @@ server {
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

location / {
proxy_pass http://frontend:80;
proxy_set_header Host $host;
Expand Down
6 changes: 6 additions & 0 deletions ml_core/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . .


RUN groupadd -r app && useradd -r -g app -d /app -s /sbin/nologin app

RUN chown -R app:app /app

USER app

CMD ["python", "worker.py"]
Loading