Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
107 changes: 49 additions & 58 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,87 +1,78 @@
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@v3

- name: Log in to the Container registry
uses: docker/login-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
with:
registry: ${{ env.REGISTRY }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

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

- name: Build and push Backend
- name: Build and push Docker images
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

deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Deploy to VPS via SSH
uses: appleboy/ssh-action@v1.0.3
- name: Deploy to VPS
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /opt/handwriter

echo "DATABASE_URL=${{ secrets.PROD_DATABASE_URL }}" > .env.tmp

git pull origin main
echo "JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}" >> .env.tmp
echo "MINIO_ROOT_PASSWORD=${{ secrets.MINIO_ROOT_PASSWORD }}" >> .env.tmp
echo "PROD_DB_PASSWORD=${{ secrets.PROD_DB_PASSWORD }}" >> .env.tmp
echo "PROD_DB_USER=${{ secrets.PROD_DB_USER }}" >> .env.tmp
echo "PROD_REDIS_URL=${{ secrets.PROD_REDIS_URL }}" >> .env.tmp
echo "TELEGRAM_BOT_TOKEN=${{ secrets.TELEGRAM_BOT_TOKEN }}" >> .env.tmp
echo "TELEGRAM_BOT_USERNAME=${{ secrets.TELEGRAM_BOT_USERNAME }}" >> .env.tmp
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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
chmod 600 .env.tmp
mv .env.tmp .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"]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
40 changes: 31 additions & 9 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", "curl", "-f", "http://localhost:8000/api/health"]
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,23 @@ 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
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 alias set myminio http://minio:9000 admin $${MINIO_ROOT_PASSWORD};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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
6 changes: 6 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@ RUN echo 'server { listen 80; location / { root /usr/share/nginx/html; index ind

EXPOSE 80

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

RUN chown -R appuser:appgroup /app

USER appuser
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

CMD ["nginx", "-g", "daemon off;"]
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